changeset 263:ee04e4e69fed

Add functional JRPG frame and tools Add full-screen background_2 apertures, functional frame chrome, card-driven details, dual-window tools, live conversion workflows, and bounded cleanup for generated downloads. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <mrjunejune@users.noreply.github.com>
date Thu, 06 Aug 2026 11:31:30 -0700
parents 0f45474c1b1a
children 04fee26ecce0
files mrjunejune/main.c mrjunejune/src/jrpg/index.html mrjunejune/src/jrpg/jrpg.css mrjunejune/src/jrpg/jrpg.js mrjunejune/src/public/jrpg/background-frame-2.webp mrjunejune/src/sw.js mrjunejune/test/BUILD mrjunejune/test/snapshots/jrpg.snapshot mrjunejune/test/theme_and_webp_test.js
diffstat 9 files changed, 2233 insertions(+), 203 deletions(-) [+]
line wrap: on
line diff
--- a/mrjunejune/main.c	Thu Aug 06 04:08:45 2026 -0700
+++ b/mrjunejune/main.c	Thu Aug 06 11:31:30 2026 -0700
@@ -691,6 +691,67 @@
   return resp;
 }
 
+static boolean Converted_Filename_Is_Valid(const char *filename)
+{
+  if (!filename)
+    return FALSE;
+  const char *extension = strrchr(filename, '.');
+  if (!extension || (strcmp(extension, ".webp") != 0 &&
+                     strcmp(extension, ".mp4") != 0))
+    return FALSE;
+  if ((size_t)(extension - filename) != 36)
+    return FALSE;
+  for (size_t i = 0; i < 36; i++)
+  {
+    char value = filename[i];
+    if (i == 8 || i == 13 || i == 18 || i == 23)
+    {
+      if (value != '-')
+        return FALSE;
+      continue;
+    }
+    if (!((value >= '0' && value <= '9') ||
+          (value >= 'a' && value <= 'f') ||
+          (value >= 'A' && value <= 'F')))
+      return FALSE;
+  }
+  return TRUE;
+}
+
+static Seobeo_Request_Entry *Converted_File_Error(
+    Dowa_Arena *arena,
+    char *status,
+    char *message)
+{
+  Seobeo_Request_Entry *resp = NULL;
+  Dowa_HashMap_Push_Arena(resp, "status", status, arena);
+  Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", message, arena);
+  return resp;
+}
+
+Seobeo_Request_Entry *DeleteConvertedFile(
+    Seobeo_Request_Entry *req,
+    Dowa_Arena *arena)
+{
+  void *filename_kv = Dowa_HashMap_Get_Ptr(req, ":filename");
+  const char *filename = filename_kv
+      ? ((Seobeo_Request_Entry *)filename_kv)->value
+      : NULL;
+  if (!Converted_Filename_Is_Valid(filename))
+    return Converted_File_Error(arena, "400", "Invalid converted filename");
+
+  char filepath[512];
+  snprintf(filepath, sizeof(filepath), "/tmp/%s", filename);
+  if (unlink(filepath) != 0 && errno != ENOENT)
+    return Converted_File_Error(arena, "500", "Unable to delete converted file");
+
+  Seobeo_Request_Entry *resp = NULL;
+  Dowa_HashMap_Push_Arena(resp, "status", "204", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", "", arena);
+  return resp;
+}
+
 Seobeo_Request_Entry *DownloadConvertedFile(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
   Seobeo_Request_Entry *resp = NULL;
@@ -706,26 +767,8 @@
   }
 
   const char *filename = ((Seobeo_Request_Entry*)filename_kv)->value;
-
-  // TODO: Maybe check if the uuid is allowed or not?
-  // if (strlen(filename) != TMP_FILE_LENGTH)
-  // {
-  //   char *error_msg = "Not Allowed Filename";
-  //   Dowa_HashMap_Push_Arena(resp, "status", "404", arena);
-  //   Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
-  //   Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena);
-  //   return resp;
-  // }
-  // boolean allowed = FALSE;
-  // for (int i = 0; i < Dowa_Array_Length(g_uuid4_array); i++)
-  // {
-  //   if strcmp(g_uuid4_array, filename)
-  //   {
-  //     allowed = TRUE;
-  //     break;
-  //   }
-  //   g_uuid4_array++;
-  // }
+  if (!Converted_Filename_Is_Valid(filename))
+    return Converted_File_Error(arena, "400", "Invalid converted filename");
 
   char filepath[512];
   snprintf(filepath, sizeof(filepath), "/tmp/%s", filename);
@@ -759,8 +802,8 @@
   file_data[file_size] = '\0';
   fclose(file);
 
-  const char *content_type = "application/octet-stream";
-  if (strstr(filename, ".webp"))
+  char *content_type = "application/octet-stream";
+  if (strcmp(strrchr(filename, '.'), ".webp") == 0)
     content_type = "image/webp";
   else if (strstr(filename, ".mp4"))
     content_type = "video/mp4";
@@ -2067,6 +2110,7 @@
   Seobeo_Router_Register("POST", "/api/convert/image-to-webp", ConvertImageToWebP);
   Seobeo_Router_Register("POST", "/api/convert/video-to-mp4", ConvertVideoToMP4);
   Seobeo_Router_Register("GET", "/api/download/:filename", DownloadConvertedFile);
+  Seobeo_Router_Register("DELETE", "/api/download/:filename", DeleteConvertedFile);
   Seobeo_Router_Register("POST", "/api/latex/render", RenderLatexPdf);
 
   // -- S3 Upload --/
--- a/mrjunejune/src/jrpg/index.html	Thu Aug 06 04:08:45 2026 -0700
+++ b/mrjunejune/src/jrpg/index.html	Thu Aug 06 11:31:30 2026 -0700
@@ -3,6 +3,7 @@
   <head>
     <title>Shiba Quest | MrJuneJune</title>
     {{/parts/base_head.html}}
+    <link rel="preload" href="/public/jrpg/background-frame-2.webp" as="image">
     <link rel="preload" href="/public/jrpg/bar-ink.webp" as="image">
     <link rel="stylesheet" href="/jrpg/jrpg.css">
     <script type="module" src="/jrpg/jrpg.js"></script>
@@ -11,6 +12,47 @@
     <main class="jrpg-main">
       <mjj-jrpg-shell class="jrpg-shell">
         <div class="jrpg-workspace">
+          <div class="jrpg-brand">
+            <header>
+              <h1>
+                <a href="/" data-zen-link-effect="none">MrJuneJune</a>
+              </h1>
+            </header>
+          </div>
+          <nav class="jrpg-frame-controls" aria-label="Window controls">
+            <zen-button appearance="plain" size="xs">
+              <button
+                type="button"
+                data-frame-minimize
+                aria-label="Minimize interface"
+                aria-pressed="false"
+              >
+                <zen-icon name="arrow-down"></zen-icon>
+              </button>
+            </zen-button>
+            <zen-button appearance="plain" size="xs">
+              <button
+                type="button"
+                data-frame-fullscreen
+                aria-label="Toggle fullscreen"
+                aria-pressed="false"
+              >
+                <zen-icon name="external"></zen-icon>
+              </button>
+            </zen-button>
+            <zen-button appearance="plain" size="xs">
+              <a href="/" aria-label="Close interface and return home">
+                <zen-icon name="close"></zen-icon>
+              </a>
+            </zen-button>
+          </nav>
+          <footer class="jrpg-frame-telemetry" aria-label="System telemetry">
+            <span aria-label="Temperature unavailable">N/A</span>
+            <span aria-label="User June">JUNE</span>
+            <span data-frame-network aria-label="Status online">ONLINE</span>
+            <time data-frame-uptime aria-label="Uptime">00:00:00</time>
+          </footer>
+
           <section class="jrpg-scene" aria-label="Shiba Quest conversation">
             <mjj-jrpg-character state="default">
               <img data-character alt="">
@@ -59,15 +101,6 @@
           </section>
 
           <section class="jrpg-utility" aria-label="Selected destination">
-            <div class="jrpg-brand">
-              <header>
-                <h1>
-                  <a href="/" data-zen-link-effect="none">MrJuneJune</a>
-                </h1>
-              </header>
-              <p>SHIBA QUEST TERMINAL</p>
-            </div>
-
             <mjj-jrpg-preview data-selection="resume">
               <article class="jrpg-preview-panel" aria-live="polite">
                 <p class="jrpg-preview-kicker" data-preview-kicker>CHARACTER RECORD</p>
@@ -89,32 +122,26 @@
                     </a>
                   </li>
                 </ul>
-                <dl class="jrpg-preview-stats">
-                  <div>
-                    <dt>TYPE</dt>
-                    <dd data-preview-type>Profile</dd>
-                  </div>
-                  <div>
-                    <dt>STATUS</dt>
-                    <dd>Available</dd>
-                  </div>
-                </dl>
-
                 <zen-dialog class="jrpg-preview-dialog">
-                  <zen-button appearance="plain" size="md">
-                    <button type="button" data-zen-trigger>
-                      Inspect
-                      <zen-icon name="search"></zen-icon>
-                    </button>
+                  <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>
-                      <zen-button appearance="plain" size="md">
-                        <button type="button" data-zen-close aria-label="Close destination details">
-                          <zen-icon name="close"></zen-icon>
-                        </button>
-                      </zen-button>
+                      <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>
+                          </a>
+                        </zen-button>
+                        <zen-button appearance="plain" size="md">
+                          <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>
@@ -143,13 +170,18 @@
                         <div data-blog-content></div>
                       </article>
                     </section>
-                    <div class="jrpg-dialog-actions">
-                      <zen-button appearance="plain" size="lg">
-                        <a href="/resume" data-preview-link>
-                          Open full page
-                          <zen-icon name="arrow-right"></zen-icon>
-                        </a>
-                      </zen-button>
+                    <section
+                      class="jrpg-tool-browser"
+                      data-tool-browser
+                      aria-label="Tool workspace"
+                      hidden
+                    >
+                      <article>
+                        <p data-tool-status>Select a tool to inspect.</p>
+                        <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
@@ -177,13 +209,13 @@
                 ></textarea>
               </zen-field>
               <div class="jrpg-composer-actions">
-                <zen-button appearance="plain" size="lg">
+                <zen-button appearance="plain" size="sm">
                   <button type="submit">
                     Send
                     <zen-icon name="arrow-right"></zen-icon>
                   </button>
                 </zen-button>
-                <zen-button appearance="plain" size="lg" data-cancel-control hidden>
+                <zen-button appearance="plain" size="sm" data-cancel-control hidden>
                   <button type="button" data-cancel>
                     Cancel
                     <zen-icon name="close"></zen-icon>
@@ -199,7 +231,7 @@
               <p class="jrpg-menu-label">Menu</p>
               <ul>
                 <li>
-                  <zen-button appearance="plain" size="lg">
+                  <zen-button appearance="plain" size="sm">
                     <button type="button" data-preview="resume" aria-pressed="true">
                       <zen-icon name="chevron-right"></zen-icon>
                       Resume
@@ -207,7 +239,7 @@
                   </zen-button>
                 </li>
                 <li>
-                  <zen-button appearance="plain" size="lg">
+                  <zen-button appearance="plain" size="sm">
                     <button type="button" data-preview="tools" aria-pressed="false">
                       <zen-icon name="chevron-right"></zen-icon>
                       Tools
@@ -215,7 +247,7 @@
                   </zen-button>
                 </li>
                 <li>
-                  <zen-button appearance="plain" size="lg">
+                  <zen-button appearance="plain" size="sm">
                     <button type="button" data-preview="blog" aria-pressed="false">
                       <zen-icon name="chevron-right"></zen-icon>
                       Blogs
--- a/mrjunejune/src/jrpg/jrpg.css	Thu Aug 06 04:08:45 2026 -0700
+++ b/mrjunejune/src/jrpg/jrpg.css	Thu Aug 06 11:31:30 2026 -0700
@@ -6,6 +6,7 @@
 
 :root {
   --mjj-jrpg-art: url("/public/jrpg/bar-ink.webp");
+  --mjj-jrpg-frame-art: url("/public/jrpg/background-frame-2.webp");
   --mjj-jrpg-canvas: var(--zenbu-sys-color-surface-page);
   --mjj-jrpg-surface: var(--zenbu-sys-color-surface-default);
   --mjj-jrpg-surface-raised: var(--zenbu-sys-color-surface-raised);
@@ -55,6 +56,12 @@
   font-family: inherit;
 }
 
+.jrpg-page zen-icon svg {
+  shape-rendering: crispEdges;
+  stroke-linecap: square;
+  stroke-linejoin: miter;
+}
+
 .jrpg-main {
   box-sizing: border-box;
   width: 100%;
@@ -65,29 +72,243 @@
 }
 
 .jrpg-shell {
+  --mjj-frame-scene-inline: 3.2%;
+  --mjj-frame-scene-block: 9.4%;
+  --mjj-frame-scene-width: 54.8%;
+  --mjj-frame-scene-height: 57%;
+  --mjj-frame-utility-inline: 60.4%;
+  --mjj-frame-utility-block: 9.4%;
+  --mjj-frame-utility-width: 36.5%;
+  --mjj-frame-utility-height: 57%;
+  --mjj-frame-composer-inline: 3.2%;
+  --mjj-frame-composer-block: 72%;
+  --mjj-frame-composer-width: 68.2%;
+  --mjj-frame-composer-height: 17.2%;
+  --mjj-frame-menu-inline: 73.4%;
+  --mjj-frame-menu-block: 72%;
+  --mjj-frame-menu-width: 23.4%;
+  --mjj-frame-menu-height: 17.2%;
+  --mjj-frame-brand-inline: 5.4%;
+  --mjj-frame-brand-block: 2.2%;
+  --mjj-frame-brand-width: 7.8%;
+  --mjj-frame-brand-height: 5.2%;
+  --mjj-frame-controls-inline: 86.6%;
+  --mjj-frame-controls-block: 3.4%;
+  --mjj-frame-controls-width: 11.5%;
+  --mjj-frame-controls-height: 10%;
+  --mjj-frame-controls-gap: 7%;
+  --mjj-frame-telemetry-inline: 8.5%;
+  --mjj-frame-telemetry-block: 92.8%;
+  --mjj-frame-telemetry-width: 75%;
+  --mjj-frame-telemetry-height: 4.8%;
+  --mjj-frame-temp-value-inline: 12%;
+  --mjj-frame-user-value-inline: 32.5%;
+  --mjj-frame-status-value-inline: 60.5%;
+  --mjj-frame-uptime-value-inline: 88%;
+  --mjj-frame-dog-bar-block-end: 4%;
+  --mjj-frame-dog-bar-height: 31%;
   box-sizing: border-box;
-  display: block;
+  display: grid;
+  place-items: center;
   width: 100%;
   max-width: none;
   height: 100%;
   margin: 0;
-  padding: var(--zenbu-sys-padding-sm);
-  border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-frame);
+  padding: 0;
+  border: 0;
   background: var(--mjj-jrpg-canvas);
-  box-shadow:
-    inset 0 0 0 var(--zenbu-sys-stroke-width)
-    var(--mjj-jrpg-frame-soft);
+  box-shadow: none;
 }
 
 .jrpg-workspace {
+  position: relative;
+  display: block;
+  width: 100%;
+  height: 100%;
+  max-height: none;
+  aspect-ratio: auto;
+  min-height: 0;
+  background-image: var(--mjj-jrpg-frame-art);
+  background-position: center;
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+  image-rendering: pixelated;
+}
+
+.jrpg-workspace > .jrpg-brand {
+  position: absolute;
+  z-index: 4;
+  inset:
+    var(--mjj-frame-brand-block)
+    auto
+    auto
+    var(--mjj-frame-brand-inline);
   display: grid;
-  grid-template:
-    "scene utility" minmax(0, 1fr)
-    "composer menu" minmax(9rem, auto) /
-    minmax(0, 1.22fr) minmax(19rem, 0.95fr);
-  gap: var(--zenbu-sys-space-group);
+  place-items: center;
+  width: var(--mjj-frame-brand-width);
+  height: var(--mjj-frame-brand-height);
+  margin: 0;
+  padding: 0;
+  border: 0;
+  background: var(--mjj-jrpg-canvas);
+  box-shadow: none;
+  text-align: center;
+}
+
+.jrpg-workspace > .jrpg-brand header {
+  position: relative;
+  z-index: 1;
+}
+
+.jrpg-frame-controls {
+  position: absolute;
+  z-index: 5;
+  inset:
+    var(--mjj-frame-controls-block)
+    auto
+    auto
+    var(--mjj-frame-controls-inline);
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: var(--mjj-frame-controls-gap);
+  width: var(--mjj-frame-controls-width);
+  height: var(--mjj-frame-controls-height);
+  margin: 0;
+}
+
+.jrpg-frame-controls zen-button,
+.jrpg-frame-controls :where(button, a) {
+  width: 100%;
   height: 100%;
-  min-height: 0;
+}
+
+.jrpg-frame-controls :where(button, a) {
+  padding: 0;
+  border: 0;
+  border-radius: 0;
+  background: transparent;
+  color: transparent;
+  cursor: pointer;
+}
+
+.jrpg-frame-controls zen-icon {
+  opacity: 0;
+}
+
+.jrpg-frame-telemetry {
+  position: absolute;
+  z-index: 4;
+  inset:
+    var(--mjj-frame-telemetry-block)
+    auto
+    auto
+    var(--mjj-frame-telemetry-inline);
+  display: block;
+  width: var(--mjj-frame-telemetry-width);
+  height: var(--mjj-frame-telemetry-height);
+  margin: 0;
+  padding: 0;
+  color: var(--mjj-jrpg-primary);
+  font-size: clamp(
+    var(--zenbu-sys-font-size-xs),
+    1vw,
+    var(--zenbu-sys-font-size-sm)
+  );
+}
+
+.jrpg-frame-telemetry > * {
+  position: absolute;
+  top: 50%;
+  padding-inline: var(--zenbu-sys-padding-md);
+  background: var(--mjj-jrpg-canvas);
+  transform: translate(-50%, -50%);
+  white-space: nowrap;
+}
+
+.jrpg-frame-telemetry > :nth-child(1) {
+  left: var(--mjj-frame-temp-value-inline);
+}
+
+.jrpg-frame-telemetry > :nth-child(2) {
+  left: var(--mjj-frame-user-value-inline);
+  padding-inline: var(--zenbu-sys-padding-xl);
+}
+
+.jrpg-frame-telemetry > :nth-child(3) {
+  left: var(--mjj-frame-status-value-inline);
+  padding-inline: var(--zenbu-sys-padding-xl);
+}
+
+.jrpg-frame-telemetry > :nth-child(4) {
+  left: var(--mjj-frame-uptime-value-inline);
+}
+
+.jrpg-frame-telemetry [data-frame-network][data-state="offline"] {
+  color: var(--mjj-jrpg-accent);
+}
+
+.jrpg-frame-controls :where(button, a):focus-visible {
+  outline-color: var(--mjj-jrpg-focus);
+}
+
+.jrpg-shell[data-minimized="true"] .jrpg-workspace > :is(
+  .jrpg-scene,
+  .jrpg-utility,
+  mjj-jrpg-composer,
+  mjj-jrpg-menu
+) {
+  visibility: hidden;
+}
+
+.jrpg-workspace > .jrpg-scene,
+.jrpg-workspace > .jrpg-utility,
+.jrpg-workspace > mjj-jrpg-composer,
+.jrpg-workspace > mjj-jrpg-menu {
+  position: absolute;
+  margin: 0;
+  border: 0;
+  border-radius: 0;
+  box-shadow: none;
+}
+
+.jrpg-workspace > .jrpg-scene {
+  inset:
+    var(--mjj-frame-scene-block)
+    auto
+    auto
+    var(--mjj-frame-scene-inline);
+  width: var(--mjj-frame-scene-width);
+  height: var(--mjj-frame-scene-height);
+}
+
+.jrpg-workspace > .jrpg-utility {
+  inset:
+    var(--mjj-frame-utility-block)
+    auto
+    auto
+    var(--mjj-frame-utility-inline);
+  width: var(--mjj-frame-utility-width);
+  height: var(--mjj-frame-utility-height);
+}
+
+.jrpg-workspace > mjj-jrpg-composer {
+  inset:
+    var(--mjj-frame-composer-block)
+    auto
+    auto
+    var(--mjj-frame-composer-inline);
+  width: var(--mjj-frame-composer-width);
+  height: var(--mjj-frame-composer-height);
+}
+
+.jrpg-workspace > mjj-jrpg-menu {
+  inset:
+    var(--mjj-frame-menu-block)
+    auto
+    auto
+    var(--mjj-frame-menu-inline);
+  width: var(--mjj-frame-menu-width);
+  height: var(--mjj-frame-menu-height);
 }
 
 .jrpg-scene,
@@ -102,12 +323,13 @@
 
 .jrpg-scene {
   grid-area: scene;
+  position: relative;
   display: grid;
-  grid-template-columns: minmax(8rem, 0.38fr) minmax(15rem, 0.92fr);
+  grid-template-columns: minmax(5rem, 0.38fr) minmax(0, 0.92fr);
   align-items: end;
-  gap: var(--zenbu-sys-space-group);
+  gap: var(--zenbu-sys-space-control);
   min-height: 0;
-  padding: var(--zenbu-sys-padding-lg);
+  padding: var(--zenbu-sys-padding-md);
   overflow: hidden;
   background-image:
     linear-gradient(
@@ -124,7 +346,23 @@
   background-size: cover;
 }
 
+.jrpg-scene::before {
+  position: absolute;
+  z-index: 0;
+  inset: auto 0 var(--mjj-frame-dog-bar-block-end);
+  height: var(--mjj-frame-dog-bar-height);
+  background-image: var(--mjj-jrpg-art);
+  background-position: center;
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+  content: "";
+  image-rendering: pixelated;
+  pointer-events: none;
+}
+
 mjj-jrpg-character {
+  position: relative;
+  z-index: 2;
   display: block;
   align-self: end;
   min-width: 0;
@@ -156,6 +394,7 @@
 
 mjj-jrpg-chat {
   position: relative;
+  z-index: 1;
   display: grid;
   grid-template-rows: auto minmax(0, 1fr) auto;
   align-self: stretch;
@@ -180,8 +419,7 @@
 .jrpg-preview-kicker,
 .jrpg-menu-label,
 .jrpg-brand > p,
-.jrpg-dialog-heading p,
-.jrpg-preview-stats dt {
+.jrpg-dialog-heading p {
   color: var(--mjj-jrpg-text-muted);
   font-size: var(--zenbu-sys-font-size-xs);
   letter-spacing: 0.08em;
@@ -317,8 +555,8 @@
 .jrpg-utility {
   grid-area: utility;
   display: grid;
-  grid-template-rows: auto minmax(0, 1fr);
-  gap: var(--zenbu-sys-space-group);
+  grid-template-rows: minmax(0, 1fr);
+  gap: 0;
   padding: var(--zenbu-sys-padding-md);
   background: var(--mjj-jrpg-surface);
 }
@@ -363,9 +601,19 @@
   min-height: 0;
 }
 
+.jrpg-workspace > .jrpg-brand header h1 {
+  font-size: clamp(
+    var(--zenbu-sys-font-size-xs),
+    0.95vw,
+    var(--zenbu-sys-font-size-sm)
+  );
+  line-height: 1;
+  white-space: nowrap;
+}
+
 .jrpg-preview-panel {
   display: grid;
-  grid-template-rows: auto auto auto minmax(0, 1fr) auto auto;
+  grid-template-rows: auto auto auto minmax(0, 1fr) auto;
   gap: var(--zenbu-sys-space-group);
   height: 100%;
   padding: var(--zenbu-sys-padding-lg);
@@ -427,22 +675,14 @@
   font-size: var(--zenbu-sys-font-size-xs);
 }
 
-.jrpg-preview-stats {
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  gap: var(--zenbu-sys-space-control);
-  margin: 0;
+.jrpg-work-showcase a:has(> zen-icon) {
+  grid-template-columns: auto minmax(0, 1fr);
 }
 
-.jrpg-preview-stats div {
-  padding: var(--zenbu-sys-padding-sm);
-  border: var(--zenbu-sys-stroke-width) solid var(--mjj-jrpg-frame-soft);
-  background: var(--mjj-jrpg-panel);
-}
-
-.jrpg-preview-stats dd {
-  color: var(--mjj-jrpg-success);
-  margin: var(--zenbu-sys-space-icon-label) 0 0;
+.jrpg-work-showcase a > zen-icon {
+  grid-row: 1 / 3;
+  align-self: center;
+  color: var(--mjj-jrpg-highlight);
 }
 
 .jrpg-preview-dialog > zen-button > button,
@@ -517,15 +757,25 @@
   width: auto;
 }
 
-.jrpg-dialog-heading zen-button > button {
-  width: var(--zenbu-control-height);
+.jrpg-dialog-heading-actions {
+  display: flex;
+  align-items: center;
+  gap: var(--zenbu-sys-space-control);
+}
+
+.jrpg-dialog-heading zen-button > :where(button, a) {
+  width: auto;
   min-width: var(--zenbu-control-height);
-  padding: 0;
   border-color: var(--mjj-jrpg-accent);
   background: var(--mjj-jrpg-surface);
   color: var(--mjj-jrpg-text);
 }
 
+.jrpg-dialog-heading zen-button > button {
+  width: var(--zenbu-control-height);
+  padding: 0;
+}
+
 .jrpg-preview-dialog dialog h2 {
   margin-top: var(--zenbu-sys-space-content);
 }
@@ -546,6 +796,35 @@
   gap: var(--zenbu-sys-space-group);
 }
 
+.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);
+}
+
+.jrpg-detail-dialog[data-detail-mode="tools"] > h2 {
+  display: none;
+}
+
+.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);
@@ -557,9 +836,13 @@
 
 .jrpg-resume-dossier[hidden],
 .jrpg-blog-browser[hidden],
+.jrpg-tool-browser[hidden],
+[data-dialog-trigger-owner][hidden],
 [data-dialog-copy][hidden],
 [data-blog-status][hidden],
+[data-tool-status][hidden],
 [data-resume-status][hidden],
+[data-dialog-actions][hidden],
 [data-resume-download][hidden] {
   display: none;
 }
@@ -659,7 +942,7 @@
   gap: var(--zenbu-sys-space-control);
 }
 
-.jrpg-blog-browser {
+:is(.jrpg-blog-browser, .jrpg-tool-browser) {
   display: grid;
   grid-template-columns: minmax(12rem, 0.35fr) minmax(0, 1fr);
   gap: var(--zenbu-sys-space-group);
@@ -667,8 +950,8 @@
   overflow: hidden;
 }
 
-.jrpg-blog-browser > aside,
-.jrpg-blog-browser > article {
+:is(.jrpg-blog-browser, .jrpg-tool-browser) > aside,
+:is(.jrpg-blog-browser, .jrpg-tool-browser) > article {
   min-height: 0;
   padding: var(--zenbu-sys-padding-md);
   overflow: auto;
@@ -677,13 +960,35 @@
   scrollbar-color: var(--mjj-jrpg-frame) transparent;
 }
 
-.jrpg-blog-browser h3,
-.jrpg-blog-browser [data-blog-status] {
+.jrpg-tool-browser {
+  display: block;
+  height: 100%;
+}
+
+.jrpg-tool-browser > article {
+  height: 100%;
+  padding: 0;
+  overflow: hidden;
+  border: 0;
+  background: transparent;
+}
+
+.jrpg-tool-browser [data-tool-content],
+.jrpg-functional-tool,
+.jrpg-tool-panes {
+  height: 100%;
+  min-height: 0;
+}
+
+:is(.jrpg-blog-browser, .jrpg-tool-browser) h3,
+.jrpg-blog-browser [data-blog-status],
+.jrpg-tool-browser [data-tool-status] {
   margin-top: 0;
   color: var(--mjj-jrpg-highlight);
 }
 
-.jrpg-blog-browser [data-blog-list] {
+:is(.jrpg-blog-browser, .jrpg-tool-browser)
+  :is([data-blog-list], [data-tool-list]) {
   display: grid;
   gap: var(--zenbu-sys-space-control);
   margin: 0;
@@ -691,12 +996,15 @@
   list-style: none;
 }
 
-.jrpg-blog-browser [data-blog-list] zen-button,
-.jrpg-blog-browser [data-blog-list] button {
+:is(.jrpg-blog-browser, .jrpg-tool-browser)
+  :is([data-blog-list], [data-tool-list]) zen-button,
+:is(.jrpg-blog-browser, .jrpg-tool-browser)
+  :is([data-blog-list], [data-tool-list]) button {
   width: 100%;
 }
 
-.jrpg-blog-browser [data-blog-list] button {
+:is(.jrpg-blog-browser, .jrpg-tool-browser)
+  :is([data-blog-list], [data-tool-list]) button {
   display: grid;
   justify-items: start;
   gap: var(--zenbu-sys-space-icon-label);
@@ -708,40 +1016,51 @@
   cursor: pointer;
 }
 
-.jrpg-blog-browser [data-blog-list] button:hover,
-.jrpg-blog-browser [data-blog-list] button[aria-pressed="true"] {
+:is(.jrpg-blog-browser, .jrpg-tool-browser)
+  :is([data-blog-list], [data-tool-list]) button:hover,
+:is(.jrpg-blog-browser, .jrpg-tool-browser)
+  :is([data-blog-list], [data-tool-list]) button[aria-pressed="true"] {
   border-color: var(--mjj-jrpg-accent);
   background: var(--mjj-jrpg-surface-raised);
 }
 
-.jrpg-blog-browser [data-blog-list] small {
+:is(.jrpg-blog-browser, .jrpg-tool-browser)
+  :is([data-blog-list], [data-tool-list]) small {
   color: var(--mjj-jrpg-text-muted);
   font-size: var(--zenbu-sys-font-size-xs);
 }
 
-.jrpg-blog-browser [data-blog-content] {
+:is(.jrpg-blog-browser, .jrpg-tool-browser)
+  :is([data-blog-content], [data-tool-content]) {
   color: var(--mjj-jrpg-text);
   line-height: var(--zenbu-sys-line-height-lg);
 }
 
-.jrpg-blog-browser [data-blog-content] :where(h1, h2, h3, h4) {
+:is(.jrpg-blog-browser, .jrpg-tool-browser)
+  :is([data-blog-content], [data-tool-content])
+  :where(h1, h2, h3, h4) {
   color: var(--mjj-jrpg-primary);
 }
 
-.jrpg-blog-browser [data-blog-content] a {
+:is(.jrpg-blog-browser, .jrpg-tool-browser)
+  :is([data-blog-content], [data-tool-content]) a {
   color: var(--mjj-jrpg-primary);
   text-decoration-color: var(--mjj-jrpg-accent);
   text-underline-offset: var(--zenbu-sys-space-icon-label);
 }
 
-.jrpg-blog-browser [data-blog-content] :where(img, video) {
+:is(.jrpg-blog-browser, .jrpg-tool-browser)
+  :is([data-blog-content], [data-tool-content])
+  :where(img, video) {
   display: block;
   max-width: 100%;
   height: auto;
   margin-inline: auto;
 }
 
-.jrpg-blog-browser [data-blog-content] :where(pre, blockquote) {
+:is(.jrpg-blog-browser, .jrpg-tool-browser)
+  :is([data-blog-content], [data-tool-content])
+  :where(pre, blockquote) {
   max-width: 100%;
   padding: var(--zenbu-sys-padding-md);
   overflow: auto;
@@ -749,12 +1068,265 @@
   background: var(--mjj-jrpg-panel);
 }
 
-.jrpg-blog-browser [data-blog-content] code {
+:is(.jrpg-blog-browser, .jrpg-tool-browser)
+  :is([data-blog-content], [data-tool-content]) code {
   color: var(--mjj-jrpg-success);
   white-space: pre-wrap;
   overflow-wrap: anywhere;
 }
 
+.jrpg-tool-readonly-note {
+  padding: var(--zenbu-sys-padding-sm);
+  border: var(--zenbu-sys-stroke-width) solid var(--mjj-jrpg-highlight);
+  background: var(--mjj-jrpg-panel);
+  color: var(--mjj-jrpg-highlight);
+}
+
+.jrpg-functional-tool {
+  min-height: 100%;
+}
+
+.jrpg-functional-tool [hidden] {
+  display: none;
+}
+
+.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);
+  gap: var(--zenbu-sys-space-control);
+  height: 100%;
+  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;
+}
+
+.jrpg-tool-window:first-of-type {
+  grid-column: 1;
+  grid-row: 1;
+}
+
+.jrpg-tool-window:last-of-type {
+  grid-column: 3;
+  grid-row: 1;
+}
+
+.jrpg-tool-pane {
+  display: grid;
+  grid-template-rows: auto minmax(0, 1fr) auto;
+  align-content: start;
+  gap: var(--zenbu-sys-space-control);
+  min-width: 0;
+  min-height: 0;
+  padding: var(--zenbu-sys-padding-md);
+  overflow: auto;
+  border: var(--zenbu-sys-stroke-width) solid var(--mjj-jrpg-frame-soft);
+  background: var(--mjj-jrpg-panel);
+}
+
+.jrpg-tool-window {
+  border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-primary);
+  box-shadow:
+    inset 0 0 0 var(--zenbu-sys-stroke-width)
+    var(--mjj-jrpg-info),
+    0 0 0 var(--zenbu-sys-stroke-width-emphasis)
+    var(--mjj-jrpg-accent),
+    0 0 var(--zenbu-sys-space-control)
+    color-mix(in srgb, var(--mjj-jrpg-info) 68%, transparent);
+}
+
+.jrpg-tool-pane h2,
+.jrpg-tool-pane-heading {
+  margin: 0;
+  color: var(--mjj-jrpg-primary);
+}
+
+.jrpg-tool-pane h2 {
+  display: flex;
+  align-items: center;
+  gap: var(--zenbu-sys-space-icon-label);
+}
+
+.jrpg-tool-pane h2 zen-icon {
+  color: var(--mjj-jrpg-highlight);
+}
+
+.jrpg-tool-pane-heading {
+  display: flex;
+  align-items: baseline;
+  justify-content: space-between;
+  gap: var(--zenbu-sys-space-control);
+}
+
+.jrpg-tool-pane-heading h2 {
+  margin: 0;
+}
+
+.jrpg-functional-tool zen-field[appearance="plain"] {
+  display: grid;
+  grid-template-rows: auto minmax(0, 1fr);
+  gap: var(--zenbu-sys-space-icon-label);
+  min-height: 0;
+}
+
+.jrpg-functional-tool :where(input, textarea) {
+  box-sizing: border-box;
+  width: 100%;
+  min-height: var(--zenbu-control-height);
+  padding:
+    var(--zenbu-control-padding-block)
+    var(--zenbu-control-padding-inline);
+  border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-frame);
+  border-radius: 0;
+  background: var(--mjj-jrpg-surface-sunken);
+  color: var(--mjj-jrpg-text);
+  font: inherit;
+}
+
+.jrpg-functional-tool textarea {
+  height: 100%;
+  min-height: 22rem;
+  resize: vertical;
+}
+
+.jrpg-functional-tool zen-button > :where(button, a) {
+  width: 100%;
+  border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-frame);
+  border-radius: 0;
+  background: var(--mjj-jrpg-primary);
+  color: var(--mjj-jrpg-primary-foreground);
+  cursor: pointer;
+}
+
+.jrpg-functional-tool zen-button > :where(button, a):hover {
+  border-color: var(--mjj-jrpg-primary-hover);
+  background: var(--mjj-jrpg-primary-hover);
+}
+
+.jrpg-functional-tool button:disabled {
+  opacity: var(--zenbu-sys-opacity-disabled);
+  cursor: not-allowed;
+}
+
+.jrpg-tool-actions {
+  display: flex;
+  gap: var(--zenbu-sys-space-control);
+}
+
+.jrpg-tool-actions zen-button {
+  flex: 1;
+}
+
+.jrpg-tool-status {
+  margin-top: 0;
+  padding: var(--zenbu-sys-padding-sm);
+  border-left: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-info);
+  background: var(--mjj-jrpg-panel);
+  color: var(--mjj-jrpg-text);
+}
+
+.jrpg-tool-status[data-state="ready"] {
+  border-left-color: var(--mjj-jrpg-success);
+  color: var(--mjj-jrpg-success);
+}
+
+.jrpg-tool-status[data-state="working"] {
+  border-left-color: var(--mjj-jrpg-highlight);
+  color: var(--mjj-jrpg-highlight);
+}
+
+.jrpg-tool-status[data-state="error"] {
+  border-left-color: var(--mjj-jrpg-accent);
+  color: var(--mjj-jrpg-accent);
+}
+
+.jrpg-markdown-output {
+  min-height: 22rem;
+  padding: var(--zenbu-sys-padding-md);
+  overflow: auto;
+  border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-frame);
+  background: var(--mjj-jrpg-surface-sunken);
+}
+
+.jrpg-markdown-output :where(h1, h2, h3) {
+  color: var(--mjj-jrpg-primary);
+}
+
+.jrpg-markdown-output code {
+  color: var(--mjj-jrpg-success);
+}
+
+.jrpg-functional-tool :where(video, iframe) {
+  width: 100%;
+  min-height: 22rem;
+  border: var(--zenbu-sys-stroke-width-emphasis) solid var(--mjj-jrpg-frame);
+  background: var(--mjj-jrpg-canvas);
+}
+
+.jrpg-tool-details {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: var(--zenbu-sys-space-control);
+  margin: 0;
+}
+
+.jrpg-tool-details div {
+  padding: var(--zenbu-sys-padding-sm);
+  border: var(--zenbu-sys-stroke-width) solid var(--mjj-jrpg-frame-soft);
+}
+
+.jrpg-tool-details dd {
+  margin: var(--zenbu-sys-space-icon-label) 0 0;
+  color: var(--mjj-jrpg-success);
+}
+
+.jrpg-functional-tool [data-converter-download] {
+  color: var(--mjj-jrpg-primary);
+  text-decoration-color: var(--mjj-jrpg-accent);
+}
+
 mjj-jrpg-composer {
   grid-area: composer;
   display: block;
@@ -928,7 +1500,48 @@
   opacity: 1;
 }
 
-@media (max-width: 52rem) {
+.jrpg-workspace > :is(
+  .jrpg-scene,
+  .jrpg-utility,
+  mjj-jrpg-composer,
+  mjj-jrpg-menu
+),
+.jrpg-scene > mjj-jrpg-chat,
+.jrpg-utility > mjj-jrpg-preview > .jrpg-preview-panel {
+  border: 0;
+  background-color: transparent;
+  box-shadow: none;
+}
+
+.jrpg-workspace > .jrpg-scene {
+  background-image: none;
+}
+
+.jrpg-workspace > mjj-jrpg-composer,
+.jrpg-workspace > mjj-jrpg-menu {
+  padding: var(--zenbu-sys-padding-sm);
+}
+
+.jrpg-workspace > mjj-jrpg-composer textarea {
+  min-height: 0;
+}
+
+.jrpg-scene .jrpg-panel-heading,
+.jrpg-scene .jrpg-message,
+.jrpg-scene .jrpg-turn-navigation,
+.jrpg-brand,
+.jrpg-work-showcase a,
+.jrpg-preview-panel > .jrpg-preview-dialog > zen-button > button,
+mjj-jrpg-composer > .jrpg-composer textarea,
+mjj-jrpg-composer > .jrpg-composer zen-button > button,
+mjj-jrpg-menu nav button {
+  border: 0;
+}
+
+@media
+  (max-width: 52rem),
+  (max-height: 40rem),
+  (min-aspect-ratio: 2 / 1) {
   .jrpg-page {
     overflow-y: auto;
   }
@@ -945,14 +1558,82 @@
   }
 
   .jrpg-workspace {
+    display: grid;
     grid-template:
+      "brand" auto
       "scene" auto
       "utility" auto
       "menu" auto
-      "composer" auto /
+      "composer" auto
+      "telemetry" auto /
       minmax(0, 1fr);
+    gap: var(--zenbu-sys-space-group);
+    width: 100%;
+    max-height: none;
     height: auto;
+    aspect-ratio: auto;
     min-height: 0;
+    background-size: cover;
+  }
+
+  .jrpg-workspace > .jrpg-scene,
+  .jrpg-workspace > .jrpg-utility,
+  .jrpg-workspace > mjj-jrpg-composer,
+  .jrpg-workspace > mjj-jrpg-menu,
+  .jrpg-workspace > .jrpg-brand,
+  .jrpg-workspace > .jrpg-frame-controls,
+  .jrpg-workspace > .jrpg-frame-telemetry {
+    position: relative;
+    inset: auto;
+    width: auto;
+    height: auto;
+  }
+
+  .jrpg-workspace > .jrpg-brand {
+    grid-area: brand;
+    justify-self: start;
+    padding: var(--zenbu-sys-padding-sm);
+    background: transparent;
+    box-shadow: none;
+  }
+
+  .jrpg-workspace > .jrpg-frame-controls {
+    grid-area: brand;
+    justify-self: end;
+    align-self: center;
+    display: flex;
+    gap: var(--zenbu-sys-space-icon-label);
+  }
+
+  .jrpg-frame-controls zen-button,
+  .jrpg-frame-controls :where(button, a) {
+    width: var(--zenbu-control-height);
+    height: var(--zenbu-control-height);
+  }
+
+  .jrpg-frame-controls :where(button, a) {
+    background: var(--mjj-jrpg-surface-raised);
+    color: var(--mjj-jrpg-text);
+  }
+
+  .jrpg-frame-controls zen-icon {
+    opacity: 1;
+  }
+
+  .jrpg-workspace > .jrpg-frame-telemetry {
+    grid-area: telemetry;
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: var(--zenbu-sys-space-control);
+    padding: var(--zenbu-sys-padding-sm);
+    background: var(--mjj-jrpg-surface-sunken);
+  }
+
+  .jrpg-frame-telemetry > * {
+    position: static;
+    justify-self: start;
+    background: transparent;
+    transform: none;
   }
 
   .jrpg-scene {
@@ -1001,10 +1682,6 @@
     align-items: center;
   }
 
-  .jrpg-brand > p {
-    max-width: 9rem;
-  }
-
   .jrpg-composer {
     grid-template-columns: 1fr;
     grid-template-rows:
@@ -1046,15 +1723,66 @@
     width: 100%;
   }
 
-  .jrpg-blog-browser {
+  :is(.jrpg-blog-browser, .jrpg-tool-browser) {
     grid-template-columns: 1fr;
     overflow: auto;
   }
 
-  .jrpg-blog-browser > aside {
+  :is(.jrpg-blog-browser, .jrpg-tool-browser) > aside {
     max-height: 16rem;
   }
 
+  .jrpg-tool-panes {
+    grid-template-columns: 1fr;
+    grid-template-rows:
+      minmax(24rem, auto)
+      calc(var(--zenbu-sys-space-container) * 2)
+      minmax(24rem, auto);
+    overflow: auto;
+  }
+
+  .jrpg-tool-panes::before {
+    grid-column: 1;
+    grid-row: 2;
+    background-image:
+      radial-gradient(
+        circle at var(--zenbu-sys-space-content) 50%,
+        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 calc(100% - var(--zenbu-sys-space-content)) 50%,
+        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(
+        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);
+  }
+
+  .jrpg-tool-window:first-of-type {
+    grid-column: 1;
+    grid-row: 1;
+  }
+
+  .jrpg-tool-window:last-of-type {
+    grid-column: 1;
+    grid-row: 3;
+  }
+
+  .jrpg-tool-pane {
+    min-height: 24rem;
+  }
+
   mjj-jrpg-menu ul {
     grid-template-columns: 1fr;
   }
@@ -1073,8 +1801,39 @@
   }
 }
 
+.jrpg-page,
+.jrpg-page * {
+  scrollbar-color: var(--mjj-jrpg-primary) transparent;
+  scrollbar-width: thin;
+}
+
+.jrpg-page::-webkit-scrollbar,
+.jrpg-page *::-webkit-scrollbar {
+  width: var(--zenbu-sys-space-related);
+  height: var(--zenbu-sys-space-related);
+}
+
+.jrpg-page::-webkit-scrollbar-track,
+.jrpg-page *::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+.jrpg-page::-webkit-scrollbar-thumb,
+.jrpg-page *::-webkit-scrollbar-thumb {
+  border: 0;
+  border-radius: 0;
+  background: var(--mjj-jrpg-primary);
+}
+
+.jrpg-page::-webkit-scrollbar-button,
+.jrpg-page *::-webkit-scrollbar-button {
+  display: none;
+}
+
 @media (forced-colors: active) {
-  .jrpg-scene {
+  .jrpg-workspace,
+  .jrpg-scene,
+  .jrpg-scene::before {
     background-image: none;
   }
 }
--- a/mrjunejune/src/jrpg/jrpg.js	Thu Aug 06 04:08:45 2026 -0700
+++ b/mrjunejune/src/jrpg/jrpg.js	Thu Aug 06 11:31:30 2026 -0700
@@ -18,10 +18,15 @@
     copy: "Experience, projects, and the systems I have helped build.",
     kicker: "CHARACTER RECORD",
     title: "Resume",
-    type: "Profile",
     url: "/resume",
     works: [
       {
+        detail: "Career dossier",
+        label: "Full resume",
+        modal: "resume",
+        url: "/resume",
+      },
+      {
         detail: "Agentic execution",
         label: "Copilot Tasks",
         url: "https://www.microsoft.com/en-us/microsoft-copilot/blog/2026/02/26/copilot-tasks-from-answers-to-actions/",
@@ -57,7 +62,6 @@
     copy: "Small, focused utilities for writing, media, and experimentation.",
     kicker: "ITEM INVENTORY",
     title: "Tools",
-    type: "Utilities",
     url: "/tools",
     works: [
       { detail: "Writing", label: "Markdown", url: "/tools/markdown_to_html" },
@@ -69,7 +73,6 @@
     copy: "Notes from building systems, games, web tools, and curious prototypes.",
     kicker: "QUEST ARCHIVE",
     title: "Blogs",
-    type: "Writing",
     url: "/blog",
     works: [
       { detail: "Archive", label: "All posts", url: "/blog" },
@@ -79,6 +82,31 @@
 
 const CONVERSATION_STORAGE_KEY = "mjj-jrpg-conversation-id";
 const TYPING_INTERVAL_MS = 18;
+const SCRIPT_LOADS = new Map();
+const TOOL_ICON_NAMES = Object.freeze({
+  "/notes": "repository",
+  "/tools/file_converter": "retry",
+  "/tools/hls_player": "play",
+  "/tools/latex_editor": "file",
+  "/tools/markdown_to_html": "code",
+});
+
+function loadScript(source) {
+  if (SCRIPT_LOADS.has(source)) return SCRIPT_LOADS.get(source);
+  const loading = new Promise((resolve, reject) => {
+    const script = document.createElement("script");
+    script.src = source;
+    script.addEventListener("load", resolve, { once: true });
+    script.addEventListener("error", () => {
+      SCRIPT_LOADS.delete(source);
+      script.remove();
+      reject(new Error(`Unable to load ${source}`));
+    }, { once: true });
+    document.head.append(script);
+  });
+  SCRIPT_LOADS.set(source, loading);
+  return loading;
+}
 
 class StreamingTextAnimator {
   constructor(render) {
@@ -443,14 +471,20 @@
   }
   for (const element of main.querySelectorAll("*")) {
     for (const attribute of [...element.attributes]) {
-      if (attribute.name.toLowerCase().startsWith("on")) {
+      const name = attribute.name.toLowerCase();
+      if (name.startsWith("on") || name === "id" || name === "style") {
         element.removeAttribute(attribute.name);
       }
     }
   }
   for (const anchor of main.querySelectorAll("a")) {
+    const href = anchor.getAttribute("href");
+    if (!href || anchor.getAttribute("aria-disabled") === "true") {
+      anchor.remove();
+      continue;
+    }
     const target = new URL(
-      anchor.getAttribute("href") || "",
+      href,
       window.location.href,
     );
     anchor.href = target.href;
@@ -469,8 +503,37 @@
   return main;
 }
 
+function sanitizeToolDetail(main) {
+  sanitizeFetchedMain(main);
+  for (const textarea of main.querySelectorAll("textarea")) {
+    const sample = document.createElement("pre");
+    sample.className = "jrpg-tool-source-sample";
+    sample.textContent = textarea.value || textarea.textContent;
+    textarea.replaceWith(sample);
+  }
+  for (const inactive of main.querySelectorAll(
+    "button, canvas, dialog, input, select, video, [hidden]",
+  )) {
+    inactive.remove();
+  }
+  for (const owner of main.querySelectorAll(
+    "zen-button, zen-checkbox, zen-field, zen-input, zen-textarea",
+  )) {
+    owner.replaceWith(...owner.childNodes);
+  }
+  for (const label of main.querySelectorAll("label[for]")) {
+    label.removeAttribute("for");
+  }
+  return main;
+}
+
 class MjjJrpgPreview extends HTMLElement {
   connectedCallback() {
+    this._dialog = this.querySelector("dialog");
+    this._onDialogClose = () => {
+      this._toolRequest = (this._toolRequest || 0) + 1;
+      this.cleanupTool();
+    };
     this._onClick = event => {
       const trigger = event.target.closest("[data-zen-trigger]");
       if (trigger) {
@@ -479,6 +542,10 @@
           this.show("blog");
           void this.loadBlogs();
         }
+        if (this.dataset.selection === "tools") {
+          this.show("tools");
+          void this.openToolWorkspace();
+        }
         return;
       }
       const blogEntry = event.target.closest("[data-blog-entry]");
@@ -491,23 +558,49 @@
         event.preventDefault();
         this.querySelector("zen-dialog").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");
+        void this.loadBlogs();
+        return;
+      }
+      const resumeModal = event.target.closest("[data-resume-modal]");
+      if (resumeModal) {
+        event.preventDefault();
+        this.querySelector("zen-dialog").open();
+        this.show("resume");
+        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();
+        void this.loadToolDetail(latestTool.dataset.toolUrl);
       }
     };
     this.addEventListener("click", this._onClick);
+    this._dialog.addEventListener("close", this._onDialogClose);
     this.show(this.dataset.selection || "resume");
   }
 
   disconnectedCallback() {
     this.removeEventListener("click", this._onClick);
+    this._dialog?.removeEventListener("close", this._onDialogClose);
+    this.cleanupTool();
   }
 
   show(selection) {
+    this.cleanupTool();
     const preview = PREVIEWS[selection] || PREVIEWS.resume;
     this.dataset.selection = selection;
     this.querySelector("[data-preview-kicker]").textContent = preview.kicker;
     this.querySelector("[data-preview-title]").textContent = preview.title;
     this.querySelector("[data-preview-copy]").textContent = preview.copy;
-    this.querySelector("[data-preview-type]").textContent = preview.type;
     this.querySelector("[data-dialog-title]").textContent = preview.title;
     this.querySelector("[data-dialog-copy]").textContent = preview.copy;
     this.renderShowcase(preview.works);
@@ -517,12 +610,18 @@
     const resumeDossier = this.querySelector("[data-resume-dossier]");
     const resumeDownload = this.querySelector("[data-resume-download]");
     const blogBrowser = this.querySelector("[data-blog-browser]");
+    const toolBrowser = this.querySelector("[data-tool-browser]");
     const isResume = selection === "resume";
     const isBlog = selection === "blog";
+    const isTools = selection === "tools";
+    this._dialog.dataset.detailMode = isTools ? "tools" : "content";
     resumeDossier.hidden = !isResume;
     resumeDownload.hidden = !isResume;
+    this.querySelector("[data-dialog-actions]").hidden = !isResume;
     blogBrowser.hidden = !isBlog;
-    this.querySelector("[data-dialog-copy]").hidden = isResume || isBlog;
+    toolBrowser.hidden = !isTools;
+    this.querySelector("[data-dialog-copy]").hidden =
+      isResume || isBlog || isTools;
     if (!isBlog) this._blogRequest = (this._blogRequest || 0) + 1;
     if (isBlog) {
       const status = this.querySelector("[data-blog-status]");
@@ -534,9 +633,17 @@
       }
       void this.loadBlogs();
     }
+    if (!isTools) this._toolRequest = (this._toolRequest || 0) + 1;
+    if (isTools) {
+      const status = this.querySelector("[data-tool-status]");
+      status.hidden = false;
+      status.textContent = "Select a tool to inspect.";
+      this.querySelector("[data-tool-content]").replaceChildren();
+      void this.loadTools();
+    }
   }
 
-  renderShowcase(works, latestBlogs = false) {
+  renderShowcase(works, latestBlogs = false, latestKind = null) {
     const showcase = this.querySelector("[data-work-showcase]");
     showcase.replaceChildren(...works.map(work => {
       const item = document.createElement("li");
@@ -545,8 +652,20 @@
       const detail = document.createElement("small");
       link.href = work.url;
       if (latestBlogs) {
-        link.dataset.latestBlog = "";
-        link.dataset.blogUrl = work.url;
+        if (work.modal === "blog") {
+          link.dataset.blogArchive = "";
+        } else {
+          link.dataset.latestBlog = "";
+          link.dataset.blogUrl = work.url;
+        }
+      }
+      if (work.modal === "resume") link.dataset.resumeModal = "";
+      if (latestKind === "tool") {
+        link.dataset.latestTool = "";
+        link.dataset.toolUrl = work.url;
+        const icon = document.createElement("zen-icon");
+        icon.setAttribute("name", TOOL_ICON_NAMES[work.url] || "settings");
+        link.prepend(icon);
       }
       if (new URL(work.url, window.location.href).origin !== window.location.origin) {
         link.target = "_blank";
@@ -592,7 +711,15 @@
   async loadBlogs() {
     if (this._blogs) {
       if (this.dataset.selection === "blog") {
-        this.renderShowcase(this._blogs.slice(0, 4), true);
+        this.renderShowcase([
+          ...this._blogs.slice(0, 4),
+          {
+            detail: "Full archive",
+            label: "All blogs",
+            modal: "blog",
+            url: "/blog",
+          },
+        ], true);
       }
       return true;
     }
@@ -622,7 +749,15 @@
       if (!this._blogs.length) throw new Error("No blog entries were found");
       this.renderBlogList();
       if (this.dataset.selection === "blog") {
-        this.renderShowcase(this._blogs.slice(0, 4), true);
+        this.renderShowcase([
+          ...this._blogs.slice(0, 4),
+          {
+            detail: "Full archive",
+            label: "All blogs",
+            modal: "blog",
+            url: "/blog",
+          },
+        ], true);
       }
       return true;
     })().catch(error => {
@@ -709,6 +844,561 @@
       status.textContent = `Unable to load blog: ${error.message}`;
     }
   }
+
+  async loadTools() {
+    if (this._tools) {
+      if (this.dataset.selection === "tools") {
+        this.renderShowcase(this._tools, false, "tool");
+      }
+      return true;
+    }
+    if (this._toolsLoading) return this._toolsLoading;
+    this._toolsLoading = (async () => {
+      const response = await fetch("/tools", {
+        headers: { Accept: "text/html" },
+      });
+      if (!response.ok) {
+        throw new Error(`Tools request failed (${response.status})`);
+      }
+      const documentCopy = new DOMParser().parseFromString(
+        await response.text(),
+        "text/html",
+      );
+      this._tools = [...documentCopy.querySelectorAll("main li a[href]")].map(
+        anchor => ({
+          detail: anchor.getAttribute("href").startsWith("/notes")
+            ? "Writing"
+            : "Utility",
+          label: anchor.textContent.trim(),
+          url: anchor.getAttribute("href"),
+        }),
+      );
+      if (!this._tools.length) throw new Error("No tools were found");
+      if (this.dataset.selection === "tools") {
+        this.renderShowcase(this._tools, false, "tool");
+      }
+      return true;
+    })().catch(error => {
+      const status = this.querySelector("[data-tool-status]");
+      status.hidden = false;
+      status.textContent = `Unable to load tools: ${error.message}`;
+      return false;
+    }).finally(() => {
+      this._toolsLoading = null;
+    });
+    return this._toolsLoading;
+  }
+
+  async openToolWorkspace() {
+    if (!await this.loadTools() || this.dataset.selection !== "tools") return;
+    await this.loadToolDetail(this._activeToolUrl || this._tools[0].url);
+  }
+
+  async loadToolDetail(url) {
+    const request = (this._toolRequest || 0) + 1;
+    this._toolRequest = request;
+    if (!await this.loadTools()) return;
+    if (request !== this._toolRequest || this.dataset.selection !== "tools") {
+      return;
+    }
+    const tool = this._tools.find(item => item.url === url);
+    const status = this.querySelector("[data-tool-status]");
+    const content = this.querySelector("[data-tool-content]");
+    if (!tool) {
+      status.hidden = false;
+      status.textContent = "Unable to load tool: unknown entry.";
+      return;
+    }
+    this._activeToolUrl = tool.url;
+    status.hidden = false;
+    status.textContent = `Loading ${tool.label}...`;
+    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;
+    }
+    if (tool.url === "/tools/file_converter") {
+      this.renderFileConverterTool(content, status);
+      return;
+    }
+    if (tool.url === "/tools/hls_player") {
+      await this.renderHlsTool(content, status, request);
+      return;
+    }
+    if (tool.url === "/tools/latex_editor") {
+      this.renderLatexTool(content, status);
+      return;
+    }
+    if (tool.url === "/notes") {
+      const heading = document.createElement("h1");
+      const description = document.createElement("p");
+      const privacy = document.createElement("p");
+      heading.textContent = "Personal Notes";
+      description.textContent =
+        "A private browser-based writing workspace with rich-text editing and server-backed persistence.";
+      privacy.textContent =
+        "The interactive editor and authentication flow are available on the full Notes page.";
+      content.replaceChildren(
+        this.createReadonlyToolNote(),
+        heading,
+        description,
+        privacy,
+      );
+      status.hidden = true;
+      return;
+    }
+    try {
+      const response = await fetch(tool.url, {
+        headers: { Accept: "text/html" },
+      });
+      if (!response.ok) {
+        throw new Error(`Tool request failed (${response.status})`);
+      }
+      const documentCopy = new DOMParser().parseFromString(
+        await response.text(),
+        "text/html",
+      );
+      const detail = documentCopy.querySelector("main, .container") ||
+        documentCopy.body;
+      if (!detail) throw new Error("Tool content is unavailable");
+      sanitizeToolDetail(detail);
+      if (request !== this._toolRequest || this.dataset.selection !== "tools") {
+        return;
+      }
+      content.replaceChildren(
+        this.createReadonlyToolNote(),
+        ...detail.childNodes,
+      );
+      status.hidden = true;
+    } catch (error) {
+      if (request !== this._toolRequest) return;
+      status.hidden = false;
+      status.textContent = `Unable to load tool: ${error.message}`;
+    }
+  }
+
+  async renderMarkdownTool(content, status, request) {
+    content.innerHTML = `
+      <section class="jrpg-functional-tool" data-functional-tool="markdown">
+        <div class="jrpg-tool-panes">
+          <section class="jrpg-tool-pane jrpg-tool-window">
+            <h2><zen-icon name="code"></zen-icon> Markdown source</h2>
+            <zen-field appearance="plain" size="md">
+              <label for="jrpgMarkdownSource">Markdown</label>
+              <textarea id="jrpgMarkdownSource" data-markdown-source></textarea>
+            </zen-field>
+          </section>
+          <section class="jrpg-tool-pane jrpg-tool-window">
+            <h2><zen-icon name="check"></zen-icon> Converted HTML</h2>
+            <div class="jrpg-markdown-output" data-markdown-output></div>
+          </section>
+        </div>
+      </section>
+    `;
+    const source = content.querySelector("[data-markdown-source]");
+    const output = content.querySelector("[data-markdown-output]");
+    source.value = [
+      "# JRPG Markdown",
+      "",
+      "Edit this **Markdown** to update the converted panel.",
+      "",
+      "- Bazel-built WASM",
+      "- Sanitized HTML output",
+    ].join("\n");
+    status.hidden = false;
+    status.textContent = "Loading the Markdown converter...";
+    try {
+      this._markdownModule ||= import("/markdown_to_html_bin.js")
+        .then(module => module.default());
+      const module = await this._markdownModule;
+      if (request !== this._toolRequest || !source.isConnected) return;
+      const convertMarkdown = module.cwrap(
+        "markdown_to_html",
+        "number",
+        ["string"],
+      );
+      const freeMarkdown = module.cwrap("markdown_free", null, ["number"]);
+      const convert = () => {
+        const pointer = convertMarkdown(source.value);
+        const html = module.UTF8ToString(pointer);
+        freeMarkdown(pointer);
+        const documentCopy = new DOMParser().parseFromString(html, "text/html");
+        sanitizeFetchedMain(documentCopy.body);
+        output.replaceChildren(...documentCopy.body.childNodes);
+      };
+      source.addEventListener("input", convert);
+      convert();
+      status.hidden = true;
+    } catch (error) {
+      this._markdownModule = null;
+      if (request !== this._toolRequest) return;
+      status.hidden = false;
+      status.textContent = `Unable to start Markdown: ${error.message}`;
+    }
+  }
+
+  renderFileConverterTool(content, status) {
+    content.innerHTML = `
+      <section class="jrpg-functional-tool" data-functional-tool="converter">
+        <p class="jrpg-tool-status" data-converter-status>
+          Choose a file in either panel.
+        </p>
+        <div class="jrpg-tool-panes">
+          <section class="jrpg-tool-pane jrpg-tool-window">
+            <h2><zen-icon name="file"></zen-icon> Image to WebP</h2>
+            <zen-field appearance="plain" size="md">
+              <label for="jrpgImageInput">Image file</label>
+              <input id="jrpgImageInput" data-converter-input="image" type="file" accept="image/*">
+            </zen-field>
+            <zen-button appearance="plain" size="md">
+              <button type="button" data-convert-kind="image">
+                Convert image <zen-icon name="arrow-right"></zen-icon>
+              </button>
+            </zen-button>
+            <a data-converter-download="image" hidden>Download WebP</a>
+          </section>
+          <section class="jrpg-tool-pane jrpg-tool-window">
+            <h2><zen-icon name="play"></zen-icon> Video to MP4</h2>
+            <zen-field appearance="plain" size="md">
+              <label for="jrpgVideoInput">Video file</label>
+              <input id="jrpgVideoInput" data-converter-input="video" type="file" accept="video/*">
+            </zen-field>
+            <zen-button appearance="plain" size="md">
+              <button type="button" data-convert-kind="video">
+                Convert video <zen-icon name="arrow-right"></zen-icon>
+              </button>
+            </zen-button>
+            <a data-converter-download="video" hidden>Download MP4</a>
+          </section>
+        </div>
+      </section>
+    `;
+    const toolStatus = content.querySelector("[data-converter-status]");
+    let disposed = false;
+    const downloads = new Set();
+    const discardDownload = url => {
+      if (!url) return;
+      downloads.delete(url);
+      void fetch(url, {
+        method: "DELETE",
+        keepalive: true,
+      }).then(response => {
+        if (!response.ok && response.status !== 404) {
+          console.warn(`Unable to discard converted file (${response.status})`);
+        }
+      }).catch(error => {
+        console.warn(`Unable to discard converted file: ${error.message}`);
+      });
+    };
+    const convert = async kind => {
+      const input = content.querySelector(`[data-converter-input="${kind}"]`);
+      const button = content.querySelector(`[data-convert-kind="${kind}"]`);
+      const download = content.querySelector(
+        `[data-converter-download="${kind}"]`,
+      );
+      const file = input.files?.[0];
+      if (!file) {
+        toolStatus.textContent = `Choose a ${kind} file first.`;
+        toolStatus.dataset.state = "error";
+        return;
+      }
+      button.disabled = true;
+      discardDownload(download.dataset.cleanupUrl);
+      delete download.dataset.cleanupUrl;
+      download.removeAttribute("href");
+      download.hidden = true;
+      toolStatus.textContent = `Converting ${file.name}...`;
+      toolStatus.dataset.state = "working";
+      try {
+        const endpoint = kind === "image"
+          ? "/api/convert/image-to-webp"
+          : "/api/convert/video-to-mp4";
+        const response = await fetch(endpoint, {
+          method: "POST",
+          body: file,
+          headers: { "Content-Type": file.type || "application/octet-stream" },
+        });
+        if (!response.ok) {
+          throw new Error(await response.text() || `Conversion failed (${response.status})`);
+        }
+        const result = await response.json();
+        if (disposed || !download.isConnected) {
+          discardDownload(result.download_url);
+          return;
+        }
+        download.href = result.download_url;
+        download.dataset.cleanupUrl = result.download_url;
+        downloads.add(result.download_url);
+        download.download = file.name.replace(/\.[^/.]+$/, "") +
+          (kind === "image" ? ".webp" : ".mp4");
+        download.hidden = false;
+        toolStatus.textContent = "Conversion complete.";
+        toolStatus.dataset.state = "ready";
+      } catch (error) {
+        if (!disposed) {
+          toolStatus.textContent = `Conversion failed: ${error.message}`;
+          toolStatus.dataset.state = "error";
+        }
+      } finally {
+        if (button.isConnected) button.disabled = false;
+      }
+    };
+    for (const button of content.querySelectorAll("[data-convert-kind]")) {
+      button.addEventListener("click", () => {
+        void convert(button.dataset.convertKind);
+      });
+    }
+    for (const download of content.querySelectorAll("[data-converter-download]")) {
+      download.addEventListener("click", () => {
+        downloads.delete(download.dataset.cleanupUrl);
+        delete download.dataset.cleanupUrl;
+      });
+    }
+    this._toolCleanup = () => {
+      disposed = true;
+      for (const download of [...downloads]) discardDownload(download);
+    };
+    status.hidden = true;
+  }
+
+  async renderHlsTool(content, status, request) {
+    content.innerHTML = `
+      <section class="jrpg-functional-tool" data-functional-tool="hls">
+        <div class="jrpg-tool-panes">
+          <section class="jrpg-tool-pane jrpg-tool-window">
+            <h2><zen-icon name="file"></zen-icon> HLS source</h2>
+            <zen-field appearance="plain" size="md">
+              <label for="jrpgHlsUrl">Playlist URL</label>
+              <input
+                id="jrpgHlsUrl"
+                data-hls-url
+                type="url"
+                value="/public/hls-sample/h264-ts-stream.m3u8"
+              >
+            </zen-field>
+            <div class="jrpg-tool-actions">
+              <zen-button appearance="plain" size="md">
+                <button type="button" data-hls-load>
+                  Load URL <zen-icon name="arrow-right"></zen-icon>
+                </button>
+              </zen-button>
+              <zen-button appearance="plain" size="md">
+                <button type="button" data-hls-sample>
+                  Load sample <zen-icon name="play"></zen-icon>
+                </button>
+              </zen-button>
+            </div>
+            <p class="jrpg-tool-status" data-hls-status>Loading player engine...</p>
+          </section>
+          <section class="jrpg-tool-pane jrpg-tool-window">
+            <h2><zen-icon name="play"></zen-icon> Playback</h2>
+            <video data-hls-video controls playsinline preload="metadata"></video>
+            <dl class="jrpg-tool-details" data-hls-details hidden>
+              <div><dt>Playback</dt><dd data-detail="mode">-</dd></div>
+              <div><dt>Segments</dt><dd data-detail="segments">-</dd></div>
+              <div><dt>Duration</dt><dd data-detail="duration">-</dd></div>
+              <div><dt>Codec</dt><dd data-detail="codecs">-</dd></div>
+            </dl>
+          </section>
+        </div>
+      </section>
+    `;
+    try {
+      await loadScript("/public/hls.min.js");
+      await loadScript("/tools/hls_player/hls-player.js");
+      if (request !== this._toolRequest || !content.isConnected) return;
+      const video = content.querySelector("[data-hls-video]");
+      const playerStatus = content.querySelector("[data-hls-status]");
+      const details = content.querySelector("[data-hls-details]");
+      const url = content.querySelector("[data-hls-url]");
+      const player = new window.HlsPlayerModule.HlsPlayer(video, {
+        statusElement: playerStatus,
+        detailsElement: details,
+      });
+      const load = () => {
+        void player.load(url.value.trim()).catch(() => {});
+      };
+      content.querySelector("[data-hls-load]").addEventListener("click", load);
+      content.querySelector("[data-hls-sample]").addEventListener("click", () => {
+        url.value = "/public/hls-sample/h264-ts-stream.m3u8";
+        load();
+      });
+      playerStatus.textContent = "Ready to load a playlist.";
+      playerStatus.dataset.state = "ready";
+      this._toolCleanup = () => player.destroy();
+      status.hidden = true;
+    } catch (error) {
+      if (request !== this._toolRequest) return;
+      status.hidden = false;
+      status.textContent = `Unable to start HLS: ${error.message}`;
+    }
+  }
+
+  renderLatexTool(content, status) {
+    content.innerHTML = `
+      <section class="jrpg-functional-tool" data-functional-tool="latex">
+        <p class="jrpg-tool-status" data-latex-status>
+          Edit the source, then compile the PDF.
+        </p>
+        <div class="jrpg-tool-panes">
+          <section class="jrpg-tool-pane jrpg-tool-window">
+            <div class="jrpg-tool-pane-heading">
+              <h2><zen-icon name="code"></zen-icon> LaTeX source</h2>
+              <span data-latex-size></span>
+            </div>
+            <zen-field appearance="plain" size="md">
+              <label for="jrpgLatexSource">document.tex</label>
+              <textarea id="jrpgLatexSource" data-latex-source></textarea>
+            </zen-field>
+            <div class="jrpg-tool-actions">
+              <zen-button appearance="plain" size="md">
+                <button type="button" data-latex-compile>
+                  Compile PDF <zen-icon name="file"></zen-icon>
+                </button>
+              </zen-button>
+              <zen-button appearance="plain" size="md" data-latex-download hidden>
+                <a download="document.pdf">
+                  Download PDF <zen-icon name="download"></zen-icon>
+                </a>
+              </zen-button>
+            </div>
+          </section>
+          <section class="jrpg-tool-pane jrpg-tool-window">
+            <h2><zen-icon name="file"></zen-icon> PDF preview</h2>
+            <iframe data-latex-preview title="Compiled LaTeX PDF preview"></iframe>
+            <pre data-latex-diagnostics hidden></pre>
+          </section>
+        </div>
+      </section>
+    `;
+    const source = content.querySelector("[data-latex-source]");
+    const compileButton = content.querySelector("[data-latex-compile]");
+    const toolStatus = content.querySelector("[data-latex-status]");
+    const size = content.querySelector("[data-latex-size]");
+    const preview = content.querySelector("[data-latex-preview]");
+    const diagnostics = content.querySelector("[data-latex-diagnostics]");
+    const downloadOwner = content.querySelector("[data-latex-download]");
+    const download = downloadOwner.querySelector("a");
+    source.value = String.raw`\documentclass[11pt]{article}
+\usepackage[margin=1in]{geometry}
+\title{JRPG LaTeX}
+\author{MrJuneJune}
+\begin{document}
+\maketitle
+This PDF was compiled from the cyberpunk tool modal.
+\end{document}`;
+    let controller = null;
+    let pdfUrl = null;
+    let compileGeneration = 0;
+    const clearPdf = () => {
+      if (pdfUrl) URL.revokeObjectURL(pdfUrl);
+      pdfUrl = null;
+      preview.src = "about:blank";
+      preview.hidden = false;
+      diagnostics.hidden = true;
+      download.removeAttribute("href");
+      downloadOwner.hidden = true;
+    };
+    const updateSize = () => {
+      const bytes = new TextEncoder().encode(source.value).byteLength;
+      size.textContent = `${bytes.toLocaleString()} / 65,536 bytes`;
+      return bytes;
+    };
+    const compile = async () => {
+      const generation = ++compileGeneration;
+      clearPdf();
+      const bytes = updateSize();
+      if (!source.value.trim() || bytes > 64 * 1024) {
+        toolStatus.textContent = bytes > 64 * 1024
+          ? "Source exceeds 64 KiB."
+          : "Write LaTeX before compiling.";
+        toolStatus.dataset.state = "error";
+        return;
+      }
+      controller?.abort();
+      const requestController = new AbortController();
+      controller = requestController;
+      compileButton.disabled = true;
+      toolStatus.textContent = "Compiling on the server...";
+      toolStatus.dataset.state = "working";
+      try {
+        const response = await fetch("/api/latex/render", {
+          method: "POST",
+          headers: { "Content-Type": "text/plain; charset=utf-8" },
+          body: source.value,
+          cache: "no-store",
+          signal: requestController.signal,
+        });
+        if (generation !== compileGeneration) return;
+        if (!response.ok) {
+          throw new Error(await response.text() || `Compilation failed (${response.status})`);
+        }
+        const blob = await response.blob();
+        if (generation !== compileGeneration) return;
+        if (blob.type !== "application/pdf" || blob.size < 5) {
+          throw new Error("The server returned an invalid PDF.");
+        }
+        if (pdfUrl) URL.revokeObjectURL(pdfUrl);
+        pdfUrl = URL.createObjectURL(blob);
+        preview.src = pdfUrl;
+        preview.hidden = false;
+        diagnostics.hidden = true;
+        download.href = pdfUrl;
+        downloadOwner.hidden = false;
+        toolStatus.textContent = `PDF ready (${Math.ceil(blob.size / 1024)} KiB).`;
+        toolStatus.dataset.state = "ready";
+      } catch (error) {
+        if (error.name !== "AbortError" && generation === compileGeneration) {
+          diagnostics.textContent = error.message;
+          diagnostics.hidden = false;
+          preview.hidden = true;
+          toolStatus.textContent = "Compilation failed.";
+          toolStatus.dataset.state = "error";
+        }
+      } finally {
+        if (controller === requestController) {
+          controller = null;
+          compileButton.disabled = false;
+        }
+      }
+    };
+    source.addEventListener("input", () => {
+      compileGeneration++;
+      controller?.abort();
+      controller = null;
+      compileButton.disabled = false;
+      clearPdf();
+      updateSize();
+      toolStatus.textContent = "Changes ready to compile.";
+      toolStatus.dataset.state = "working";
+    });
+    compileButton.addEventListener("click", () => void compile());
+    updateSize();
+    this._toolCleanup = () => {
+      compileGeneration++;
+      controller?.abort();
+      clearPdf();
+    };
+    status.hidden = true;
+  }
+
+  cleanupTool() {
+    this._toolCleanup?.();
+    this._toolCleanup = null;
+  }
+
+  createReadonlyToolNote() {
+    const introduction = document.createElement("p");
+    introduction.className = "jrpg-tool-readonly-note";
+    introduction.textContent =
+      "Read-only preview. Open the full page to use this tool.";
+    return introduction;
+  }
 }
 
 class MjjJrpgShell extends HTMLElement {}
@@ -792,10 +1482,59 @@
   const chat = shell?.querySelector("mjj-jrpg-chat");
   const composer = shell?.querySelector("mjj-jrpg-composer");
   const preview = shell?.querySelector("mjj-jrpg-preview");
+  const frameNetwork = shell?.querySelector("[data-frame-network]");
+  const frameUptime = shell?.querySelector("[data-frame-uptime]");
+  const minimizeButton = shell?.querySelector("[data-frame-minimize]");
+  const fullscreenButton = shell?.querySelector("[data-frame-fullscreen]");
   let characterTimer = 0;
   let conversationId = sessionStorage.getItem(CONVERSATION_STORAGE_KEY);
   let activeController = null;
   composer?.setDisabled(true);
+  const setFrameStatus = (label, state, title = "") => {
+    if (frameNetwork) {
+      frameNetwork.textContent = label;
+      frameNetwork.setAttribute("aria-label", `Status ${label.toLowerCase()}`);
+      frameNetwork.dataset.state = state;
+      frameNetwork.title = title;
+    }
+  };
+  setFrameStatus("ONLINE", "online");
+  const startedAt = Date.now();
+  const updateUptime = () => {
+    const elapsed = Math.floor((Date.now() - startedAt) / 1000);
+    const hours = String(Math.floor(elapsed / 3600)).padStart(2, "0");
+    const minutes = String(Math.floor((elapsed % 3600) / 60)).padStart(2, "0");
+    const seconds = String(elapsed % 60).padStart(2, "0");
+    if (frameUptime) {
+      frameUptime.textContent = `${hours}:${minutes}:${seconds}`;
+    }
+  };
+  updateUptime();
+  window.setInterval(updateUptime, 1000);
+
+  minimizeButton?.addEventListener("click", () => {
+    const minimized = shell.dataset.minimized !== "true";
+    shell.dataset.minimized = String(minimized);
+    minimizeButton.setAttribute("aria-pressed", String(minimized));
+    minimizeButton.setAttribute(
+      "aria-label",
+      minimized ? "Restore interface" : "Minimize interface",
+    );
+  });
+  fullscreenButton?.addEventListener("click", async () => {
+    try {
+      if (document.fullscreenElement) await document.exitFullscreen();
+      else await shell.requestFullscreen();
+    } catch (error) {
+      setFrameStatus("DENIED", "offline", error.message);
+    }
+  });
+  document.addEventListener("fullscreenchange", () => {
+    fullscreenButton?.setAttribute(
+      "aria-pressed",
+      String(Boolean(document.fullscreenElement)),
+    );
+  });
 
   if (conversationId) {
     try {
@@ -911,6 +1650,7 @@
       }
       chat?.setMessageStreaming(assistantItem, false);
       character?.setAttribute("state", "default");
+      setFrameStatus("ONLINE", "online");
     } catch (error) {
       const aborted = error instanceof DOMException && error.name === "AbortError";
       assistantText.cancel();
@@ -920,6 +1660,7 @@
         aborted ? "Quest cancelled." : `System error: ${error.message}`,
       );
       character?.setAttribute("state", "sad");
+      if (!aborted) setFrameStatus("OFFLINE", "offline");
     } finally {
       composer?.setBusy(false);
       composer?.querySelector("textarea")?.focus();
Binary file mrjunejune/src/public/jrpg/background-frame-2.webp has changed
--- a/mrjunejune/src/sw.js	Thu Aug 06 04:08:45 2026 -0700
+++ b/mrjunejune/src/sw.js	Thu Aug 06 11:31:30 2026 -0700
@@ -1,5 +1,5 @@
 // Root-scoped Service Worker for MrJuneJune PWA
-const CACHE_VERSION = 'v14-blog-browser';
+const CACHE_VERSION = 'v30-card-driven-details';
 const CACHE_NAME = `mrjunejune-${CACHE_VERSION}`;
 
 // Files to cache immediately on install
--- a/mrjunejune/test/BUILD	Thu Aug 06 04:08:45 2026 -0700
+++ b/mrjunejune/test/BUILD	Thu Aug 06 11:31:30 2026 -0700
@@ -124,6 +124,7 @@
     "//hg-web/e2e:node_modules/playwright-core",
     "//mrjunejune:mrjunejune_server_debug",
     ":inference_bridge_fake_sidecar",
+    "shiba.webp",
     "@playwright_chromium_linux//:chrome",
     "@playwright_chromium_linux//:chromium",
   ],
--- a/mrjunejune/test/snapshots/jrpg.snapshot	Thu Aug 06 04:08:45 2026 -0700
+++ b/mrjunejune/test/snapshots/jrpg.snapshot	Thu Aug 06 11:31:30 2026 -0700
@@ -32,6 +32,7 @@
 
 <script src="/public/pwa-register.js" defer></script>
 
+    <link rel="preload" href="/public/jrpg/background-frame-2.webp" as="image">
     <link rel="preload" href="/public/jrpg/bar-ink.webp" as="image">
     <link rel="stylesheet" href="/jrpg/jrpg.css">
     <script type="module" src="/jrpg/jrpg.js"></script>
@@ -40,6 +41,47 @@
     <main class="jrpg-main">
       <mjj-jrpg-shell class="jrpg-shell">
         <div class="jrpg-workspace">
+          <div class="jrpg-brand">
+            <header>
+              <h1>
+                <a href="/" data-zen-link-effect="none">MrJuneJune</a>
+              </h1>
+            </header>
+          </div>
+          <nav class="jrpg-frame-controls" aria-label="Window controls">
+            <zen-button appearance="plain" size="xs">
+              <button
+                type="button"
+                data-frame-minimize
+                aria-label="Minimize interface"
+                aria-pressed="false"
+              >
+                <zen-icon name="arrow-down"></zen-icon>
+              </button>
+            </zen-button>
+            <zen-button appearance="plain" size="xs">
+              <button
+                type="button"
+                data-frame-fullscreen
+                aria-label="Toggle fullscreen"
+                aria-pressed="false"
+              >
+                <zen-icon name="external"></zen-icon>
+              </button>
+            </zen-button>
+            <zen-button appearance="plain" size="xs">
+              <a href="/" aria-label="Close interface and return home">
+                <zen-icon name="close"></zen-icon>
+              </a>
+            </zen-button>
+          </nav>
+          <footer class="jrpg-frame-telemetry" aria-label="System telemetry">
+            <span aria-label="Temperature unavailable">N/A</span>
+            <span aria-label="User June">JUNE</span>
+            <span data-frame-network aria-label="Status online">ONLINE</span>
+            <time data-frame-uptime aria-label="Uptime">00:00:00</time>
+          </footer>
+
           <section class="jrpg-scene" aria-label="Shiba Quest conversation">
             <mjj-jrpg-character state="default">
               <img data-character alt="">
@@ -88,15 +130,6 @@
           </section>
 
           <section class="jrpg-utility" aria-label="Selected destination">
-            <div class="jrpg-brand">
-              <header>
-                <h1>
-                  <a href="/" data-zen-link-effect="none">MrJuneJune</a>
-                </h1>
-              </header>
-              <p>SHIBA QUEST TERMINAL</p>
-            </div>
-
             <mjj-jrpg-preview data-selection="resume">
               <article class="jrpg-preview-panel" aria-live="polite">
                 <p class="jrpg-preview-kicker" data-preview-kicker>CHARACTER RECORD</p>
@@ -118,32 +151,26 @@
                     </a>
                   </li>
                 </ul>
-                <dl class="jrpg-preview-stats">
-                  <div>
-                    <dt>TYPE</dt>
-                    <dd data-preview-type>Profile</dd>
-                  </div>
-                  <div>
-                    <dt>STATUS</dt>
-                    <dd>Available</dd>
-                  </div>
-                </dl>
-
                 <zen-dialog class="jrpg-preview-dialog">
-                  <zen-button appearance="plain" size="md">
-                    <button type="button" data-zen-trigger>
-                      Inspect
-                      <zen-icon name="search"></zen-icon>
-                    </button>
+                  <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>
-                      <zen-button appearance="plain" size="md">
-                        <button type="button" data-zen-close aria-label="Close destination details">
-                          <zen-icon name="close"></zen-icon>
-                        </button>
-                      </zen-button>
+                      <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>
+                          </a>
+                        </zen-button>
+                        <zen-button appearance="plain" size="md">
+                          <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>
@@ -172,13 +199,18 @@
                         <div data-blog-content></div>
                       </article>
                     </section>
-                    <div class="jrpg-dialog-actions">
-                      <zen-button appearance="plain" size="lg">
-                        <a href="/resume" data-preview-link>
-                          Open full page
-                          <zen-icon name="arrow-right"></zen-icon>
-                        </a>
-                      </zen-button>
+                    <section
+                      class="jrpg-tool-browser"
+                      data-tool-browser
+                      aria-label="Tool workspace"
+                      hidden
+                    >
+                      <article>
+                        <p data-tool-status>Select a tool to inspect.</p>
+                        <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
@@ -206,13 +238,13 @@
                 ></textarea>
               </zen-field>
               <div class="jrpg-composer-actions">
-                <zen-button appearance="plain" size="lg">
+                <zen-button appearance="plain" size="sm">
                   <button type="submit">
                     Send
                     <zen-icon name="arrow-right"></zen-icon>
                   </button>
                 </zen-button>
-                <zen-button appearance="plain" size="lg" data-cancel-control hidden>
+                <zen-button appearance="plain" size="sm" data-cancel-control hidden>
                   <button type="button" data-cancel>
                     Cancel
                     <zen-icon name="close"></zen-icon>
@@ -228,7 +260,7 @@
               <p class="jrpg-menu-label">Menu</p>
               <ul>
                 <li>
-                  <zen-button appearance="plain" size="lg">
+                  <zen-button appearance="plain" size="sm">
                     <button type="button" data-preview="resume" aria-pressed="true">
                       <zen-icon name="chevron-right"></zen-icon>
                       Resume
@@ -236,7 +268,7 @@
                   </zen-button>
                 </li>
                 <li>
-                  <zen-button appearance="plain" size="lg">
+                  <zen-button appearance="plain" size="sm">
                     <button type="button" data-preview="tools" aria-pressed="false">
                       <zen-icon name="chevron-right"></zen-icon>
                       Tools
@@ -244,7 +276,7 @@
                   </zen-button>
                 </li>
                 <li>
-                  <zen-button appearance="plain" size="lg">
+                  <zen-button appearance="plain" size="sm">
                     <button type="button" data-preview="blog" aria-pressed="false">
                       <zen-icon name="chevron-right"></zen-icon>
                       Blogs
--- a/mrjunejune/test/theme_and_webp_test.js	Thu Aug 06 04:08:45 2026 -0700
+++ b/mrjunejune/test/theme_and_webp_test.js	Thu Aug 06 11:31:30 2026 -0700
@@ -183,6 +183,20 @@
     }
   });
   page.on('requestfailed', request => {
+    if (request.url().startsWith('blob:') &&
+        request.failure()?.errorText === 'net::ERR_ABORTED') {
+      return;
+    }
+    if (request.method() === 'DELETE' &&
+        request.url().includes('/api/download/') &&
+        request.failure()?.errorText === 'net::ERR_ABORTED') {
+      return;
+    }
+    if (request.method() === 'POST' &&
+        request.url().endsWith('/api/latex/render') &&
+        request.failure()?.errorText === 'net::ERR_ABORTED') {
+      return;
+    }
     errors.push(`requestfailed: ${request.url()} ${request.failure()?.errorText || ''}`);
   });
   page.on('response', response => {
@@ -449,6 +463,20 @@
     }
   });
   page.on('requestfailed', request => {
+    if (request.url().startsWith('blob:') &&
+        request.failure()?.errorText === 'net::ERR_ABORTED') {
+      return;
+    }
+    if (request.method() === 'DELETE' &&
+        request.url().includes('/api/download/') &&
+        request.failure()?.errorText === 'net::ERR_ABORTED') {
+      return;
+    }
+    if (request.method() === 'POST' &&
+        request.url().endsWith('/api/latex/render') &&
+        request.failure()?.errorText === 'net::ERR_ABORTED') {
+      return;
+    }
     errors.push(`requestfailed: ${request.url()} ${request.failure()?.errorText || ''}`);
   });
   page.on('response', response => {
@@ -487,10 +515,25 @@
       .getBoundingClientRect();
     const menu = document.querySelector('mjj-jrpg-menu').getBoundingClientRect();
     const character = document.querySelector('[data-character]');
+    const characterBox = character.getBoundingClientRect();
+    const sceneBarStyle = getComputedStyle(
+      document.querySelector('.jrpg-scene'),
+      '::before',
+    );
     const messageScroller = document.querySelector('zen-message-scroller')
       .getBoundingClientRect();
     const messageViewport = document.querySelector('.jrpg-message-viewport')
       .getBoundingClientRect();
+    const workspace = document.querySelector('.jrpg-workspace')
+      .getBoundingClientRect();
+    const brand = document.querySelector('.jrpg-brand').getBoundingClientRect();
+    const frameControls = document.querySelector('.jrpg-frame-controls')
+      .getBoundingClientRect();
+    const frameTelemetry = document.querySelector('.jrpg-frame-telemetry')
+      .getBoundingClientRect();
+    const telemetryValues = [
+      ...document.querySelectorAll('.jrpg-frame-telemetry > *'),
+    ].map(value => value.getBoundingClientRect());
     const shell = document.querySelector('.jrpg-shell').getBoundingClientRect();
     const main = document.querySelector('.jrpg-main').getBoundingClientRect();
     const colorProbe = document.createElement('span');
@@ -566,8 +609,53 @@
     surfaceProbe.remove();
     sunkenProbe.remove();
     panelProbe.remove();
+    const closeTo = (value, expected) => Math.abs(value - expected) < 0.01;
+    const frameAlignment = [
+      closeTo((scene.left - workspace.left) / workspace.width, 0.032),
+      closeTo((scene.top - workspace.top) / workspace.height, 0.094),
+      closeTo(scene.width / workspace.width, 0.548),
+      closeTo(scene.height / workspace.height, 0.57),
+      closeTo((utility.left - workspace.left) / workspace.width, 0.604),
+      closeTo(utility.width / workspace.width, 0.365),
+      closeTo((composer.top - workspace.top) / workspace.height, 0.72),
+      closeTo(composer.width / workspace.width, 0.682),
+      closeTo((menu.left - workspace.left) / workspace.width, 0.734),
+      closeTo(menu.width / workspace.width, 0.234),
+    ].every(Boolean);
+    const borderless = [
+      '.jrpg-shell',
+      '.jrpg-scene',
+      '.jrpg-utility',
+      'mjj-jrpg-chat',
+      'mjj-jrpg-composer',
+      'mjj-jrpg-menu',
+      '.jrpg-preview-panel',
+      '.jrpg-message',
+      '.jrpg-work-showcase a',
+      '.jrpg-composer textarea',
+      '.jrpg-composer button[type="submit"]',
+      'mjj-jrpg-menu button',
+    ].every(selector => {
+      const style = getComputedStyle(document.querySelector(selector));
+      return style.borderTopWidth === '0px' &&
+        style.borderRightWidth === '0px' &&
+        style.borderBottomWidth === '0px' &&
+        style.borderLeftWidth === '0px';
+    });
     return {
-      art: getComputedStyle(document.querySelector('.jrpg-scene')).backgroundImage,
+      art: getComputedStyle(document.querySelector('.jrpg-workspace'))
+        .backgroundImage,
+      borderless,
+      brandInSystemSlot:
+        closeTo((brand.left - workspace.left) / workspace.width, 0.054) &&
+        closeTo((brand.top - workspace.top) / workspace.height, 0.022) &&
+        document.querySelector('.jrpg-brand').parentElement.classList
+          .contains('jrpg-workspace'),
+      brandCoversSystemLabel:
+        brand.left <= workspace.left + workspace.width * 0.062 &&
+        brand.right >= workspace.left + workspace.width * 0.102 &&
+        brand.top <= workspace.top + workspace.height * 0.03 &&
+        brand.bottom >= workspace.top + workspace.height * 0.06,
       characterLoaded: character.complete && character.naturalWidth > 0,
       colors,
       composerBelowScene: composer.top >= scene.bottom,
@@ -600,6 +688,11 @@
         main.top === 0 &&
         Math.abs(main.right - innerWidth) < 1 &&
         Math.abs(main.bottom - innerHeight) < 1,
+      frameAlignment,
+      frameChromeAligned:
+        closeTo((frameControls.left - workspace.left) / workspace.width, 0.866) &&
+        closeTo((frameTelemetry.left - workspace.left) / workspace.width, 0.085) &&
+        closeTo((frameTelemetry.top - workspace.top) / workspace.height, 0.928),
       fonts: computedFonts,
       fontLoaded: [...document.fonts].some(font =>
         font.family.replaceAll('"', '') === 'Pixel Mplus' &&
@@ -614,23 +707,72 @@
       noOverflow: document.documentElement.scrollWidth <= innerWidth,
       nonPixelText,
       questLabelAbsent: !document.body.textContent.includes('QUEST 01'),
+      statsRemoved:
+        !document.querySelector('.jrpg-preview-stats') &&
+        !document.querySelector('[data-preview-type]'),
+      telemetryCovered:
+        document.querySelectorAll('.jrpg-frame-telemetry > *').length === 4 &&
+        document.querySelector('[data-frame-network]').textContent
+          .includes('ONLINE') &&
+        document.querySelector('[data-frame-uptime]').textContent
+          .match(/^\d{2}:\d{2}:\d{2}$/) &&
+        [...document.querySelectorAll('.jrpg-frame-telemetry > *')]
+          .every(item => !/TEMP:|USER:|STATUS:|UPTIME:/.test(item.textContent)),
+      telemetryValuesAligned:
+        closeTo(
+          (telemetryValues[0].left + telemetryValues[0].width / 2 -
+            frameTelemetry.left) / frameTelemetry.width,
+          0.12,
+        ) &&
+        closeTo(
+          (telemetryValues[1].left + telemetryValues[1].width / 2 -
+            frameTelemetry.left) / frameTelemetry.width,
+          0.325,
+        ) &&
+        closeTo(
+          (telemetryValues[2].left + telemetryValues[2].width / 2 -
+            frameTelemetry.left) / frameTelemetry.width,
+          0.605,
+        ) &&
+        closeTo(
+          (telemetryValues[3].left + telemetryValues[3].width / 2 -
+            frameTelemetry.left) / frameTelemetry.width,
+          0.88,
+        ),
+      shibaInFrontOfBar:
+        sceneBarStyle.backgroundImage.includes('bar-ink.webp') &&
+        Number(getComputedStyle(character.parentElement).zIndex) >
+          Number(sceneBarStyle.zIndex) &&
+        characterBox.bottom > scene.top + scene.height * 0.65,
+      terminalScrollbar: (
+        getComputedStyle(
+          document.querySelector('.jrpg-message-viewport'),
+        ).scrollbarWidth === 'thin' &&
+        getComputedStyle(
+          document.querySelector('.jrpg-message-viewport'),
+        ).scrollbarColor !== 'auto'
+      ),
       theme: document.documentElement.dataset.zenTheme,
       utilityRightOfScene: utility.left >= scene.right,
+      workspaceFillsViewport:
+        Math.abs(workspace.left) < 1 &&
+        Math.abs(workspace.top) < 1 &&
+        Math.abs(workspace.right - innerWidth) < 1 &&
+        Math.abs(workspace.bottom - innerHeight) < 1,
     };
   });
-  assert.match(desktop.art, /bar-ink\.webp/);
+  assert.match(desktop.art, /background-frame-2\.webp/);
+  assert.equal(desktop.borderless, true);
+  assert.equal(desktop.brandInSystemSlot, true);
+  assert.equal(desktop.brandCoversSystemLabel, true);
   assert.equal(desktop.characterLoaded, true);
   assert.equal(desktop.colors.page, desktop.colors.canvas);
-  assert.equal(
-    desktop.colors.conversation,
-    desktop.colors.conversationPanel,
-  );
-  assert.equal(desktop.colors.conversationBorder, desktop.colors.frame);
+  assert.equal(desktop.colors.conversation, 'rgba(0, 0, 0, 0)');
   assert.equal(desktop.colors.conversationText, desktop.colors.text);
-  assert.equal(desktop.colors.menu, desktop.colors.sunken);
+  assert.equal(desktop.colors.menu, 'rgba(0, 0, 0, 0)');
   assert.equal(desktop.colors.primaryText, desktop.colors.primary);
   assert.equal(desktop.colors.submit, desktop.colors.primary);
-  assert.equal(desktop.colors.utility, desktop.colors.surface);
+  assert.equal(desktop.colors.utility, 'rgba(0, 0, 0, 0)');
   assert.equal(desktop.composerBelowScene, true);
   assert.equal(
     desktop.composerTextareaFills,
@@ -638,6 +780,8 @@
     JSON.stringify(desktop.composerTextareaGeometry),
   );
   assert.equal(desktop.fillsViewport, true);
+  assert.equal(desktop.frameAlignment, true);
+  assert.equal(desktop.frameChromeAligned, true);
   assert.ok(desktop.fonts.every(font => font === '"Pixel Mplus"'));
   assert.equal(desktop.fontLoaded, true);
   assert.equal(desktop.menuRightOfComposer, true);
@@ -645,8 +789,14 @@
   assert.equal(desktop.noOverflow, true);
   assert.deepEqual(desktop.nonPixelText, []);
   assert.equal(desktop.questLabelAbsent, true);
+  assert.equal(desktop.statsRemoved, true);
+  assert.equal(desktop.telemetryCovered, true);
+  assert.equal(desktop.telemetryValuesAligned, true);
+  assert.equal(desktop.shibaInFrontOfBar, true);
+  assert.equal(desktop.terminalScrollbar, true);
   assert.equal(desktop.theme, 'cyberpunk');
   assert.equal(desktop.utilityRightOfScene, true);
+  assert.equal(desktop.workspaceFillsViewport, true);
   await page.locator('.jrpg-brand a').focus();
   assert.equal(
     await page.locator('.jrpg-brand a').evaluate(element =>
@@ -665,7 +815,33 @@
     await page.locator('meta[name="theme-color"]').first().getAttribute('content'),
     desktop.colors.canvas,
   );
-  assert.equal(await page.locator('[data-work-showcase] a').count(), 6);
+  assert.equal(
+    await page.locator('.jrpg-frame-controls :is(button, a)').count(),
+    3,
+  );
+  assert.equal(
+    await page.locator('.jrpg-frame-controls a').getAttribute('href'),
+    '/',
+  );
+  await page.getByRole('button', { name: 'Minimize interface' }).click();
+  assert.equal(
+    await page.locator('.jrpg-scene').evaluate(
+      scene => getComputedStyle(scene).visibility,
+    ),
+    'hidden',
+  );
+  await page.getByRole('button', { name: 'Restore interface' }).click();
+  assert.equal(
+    await page.locator('.jrpg-scene').evaluate(
+      scene => getComputedStyle(scene).visibility,
+    ),
+    'visible',
+  );
+  assert.equal(await page.locator('[data-work-showcase] a').count(), 7);
+  assert.equal(
+    await page.getByRole('button', { name: /Inspect/ }).count(),
+    0,
+  );
   assert.match(
     await page.locator('[data-work-showcase]').textContent(),
     /Copilot Tasks/,
@@ -701,13 +877,208 @@
       .getAttribute('aria-pressed'),
     'true',
   );
-  assert.equal(await page.locator('[data-work-showcase] a').count(), 3);
-
-  await page.getByRole('button', { name: /Inspect/ }).click();
-  await page.locator('.jrpg-preview-dialog dialog[open]').waitFor();
+  await page.waitForFunction(() =>
+    document.querySelectorAll('[data-latest-tool]').length === 5
+  );
+  assert.match(
+    await page.locator('[data-work-showcase]').textContent(),
+    /Online LaTeX Editor/,
+  );
+  assert.match(
+    await page.locator('[data-work-showcase]').textContent(),
+    /Personal Notes/,
+  );
+  await page.locator('[data-latest-tool]').first().click();
+  await page.locator('[data-functional-tool="markdown"]').waitFor();
   assert.equal(
     await page.locator('[data-dialog-title]').textContent(),
-    'Tools',
+    '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 ===
+      'Modal Markdown'
+  );
+  assert.equal(await page.locator('.jrpg-tool-pane').count(), 2);
+  const toolWorkspace = await page.evaluate(() => {
+    const dialogElement = document.querySelector(
+      '.jrpg-detail-dialog[data-detail-mode="tools"]',
+    );
+    const dialog = dialogElement.getBoundingClientRect();
+    const windows = [...document.querySelectorAll('.jrpg-tool-window')]
+      .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,
+      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'),
+      pixelIcons: document.querySelectorAll('.jrpg-tool-window zen-icon').length >= 2 &&
+        [...document.querySelectorAll('.jrpg-tool-window zen-icon svg')]
+          .every(icon =>
+            getComputedStyle(icon).shapeRendering.toLowerCase() === 'crispedges'
+          ),
+      separateGlows: windowShadows.length === 2 &&
+        windowShadows.every(shadow => shadow !== 'none'),
+      wideWindows: windows.every(window => window.width > dialog.width * 0.35),
+    };
+  });
+  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.pixelIcons, true);
+  assert.equal(toolWorkspace.separateGlows, true);
+  assert.equal(toolWorkspace.wideWindows, true);
+  await page.getByRole('button', {
+    name: 'Close destination details',
+  }).click();
+  await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });
+
+  await page.locator('[data-latest-tool]').first().click();
+  await page.locator('[data-functional-tool="markdown"]').waitFor();
+  await page.getByRole('button', {
+    name: 'Close destination details',
+  }).click();
+  await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });
+
+  await page.locator('[data-latest-tool]').nth(1).click();
+  await page.locator('[data-functional-tool="converter"]').waitFor();
+  assert.equal(
+    await page.locator('[data-converter-input]').count(),
+    2,
+  );
+  const converterFixture = path.join(
+    RUNFILES,
+    WORKSPACE,
+    'mrjunejune/test/shiba.webp',
+  );
+  assert.equal(fs.existsSync(converterFixture), true);
+  await page.locator('[data-converter-input="image"]').setInputFiles(
+    converterFixture,
+  );
+  const conversionResponse = page.waitForResponse(response =>
+    response.url().endsWith('/api/convert/image-to-webp') &&
+    response.request().method() === 'POST'
+  );
+  await page.locator('[data-convert-kind="image"]').click();
+  assert.equal((await conversionResponse).status(), 200);
+  await page.locator('[data-converter-download="image"]').waitFor();
+  const cleanupResponse = page.waitForResponse(response =>
+    response.url().includes('/api/download/') &&
+    response.request().method() === 'DELETE'
+  );
+  await page.getByRole('button', {
+    name: 'Close destination details',
+  }).click();
+  assert.equal((await cleanupResponse).status(), 204);
+  await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });
+
+  await page.locator('[data-latest-tool]').nth(1).click();
+  await page.locator('[data-functional-tool="converter"]').waitFor();
+  await page.locator('[data-converter-input="image"]').setInputFiles(
+    converterFixture,
+  );
+  const lateConversionResponse = page.waitForResponse(response =>
+    response.url().endsWith('/api/convert/image-to-webp') &&
+    response.request().method() === 'POST'
+  );
+  const lateCleanupResponse = page.waitForResponse(response =>
+    response.url().includes('/api/download/') &&
+    response.request().method() === 'DELETE'
+  );
+  await page.locator('[data-convert-kind="image"]').click();
+  await page.getByRole('button', {
+    name: 'Close destination details',
+  }).click();
+  assert.equal((await lateConversionResponse).status(), 200);
+  assert.equal((await lateCleanupResponse).status(), 204);
+  await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });
+
+  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(
+      document.querySelector('[data-hls-status]')?.dataset.state,
+    )
+  );
+  assert.ok(
+    ['ready', 'error'].includes(
+      await page.locator('[data-hls-status]').getAttribute('data-state'),
+    ),
+  );
+  assert.match(
+    await page.locator('[data-hls-status]').textContent(),
+    /ready|unsupported|failed/i,
+  );
+  await page.getByRole('button', {
+    name: 'Close destination details',
+  }).click();
+  await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });
+
+  await page.locator('[data-latest-tool]').nth(3).click();
+  await page.locator('[data-functional-tool="latex"]').waitFor();
+  await page.locator('[data-latex-compile]').click();
+  await page.locator('[data-latex-source]').fill(
+    '\\documentclass{article}\\begin{document}Updated modal PDF\\end{document}',
+  );
+  assert.equal(await page.locator('[data-latex-download]').isHidden(), true);
+  await page.locator('[data-latex-compile]').click();
+  await page.waitForFunction(() =>
+    document.querySelector('[data-latex-status]')?.dataset.state === 'ready'
+  );
+  assert.match(
+    await page.locator('[data-latex-preview]').getAttribute('src'),
+    /^blob:/,
+  );
+  await page.getByRole('button', {
+    name: 'Close destination details',
+  }).click();
+  await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });
+
+  await page.locator('[data-latest-tool]').nth(4).click();
+  await page.waitForFunction(() =>
+    document.querySelector('[data-tool-content] h1')?.textContent ===
+      'Personal Notes'
+  );
+  assert.match(
+    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',
@@ -715,7 +1086,7 @@
   await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });
 
   await page.getByRole('button', { name: 'Resume', exact: true }).click();
-  await page.getByRole('button', { name: /Inspect/ }).click();
+  await page.locator('[data-resume-modal]').click();
   await page.locator('[data-resume-content] .sub-header').first().waitFor();
   const resumeDossier = await page.locator('[data-resume-dossier]').evaluate(
     dossier => ({
@@ -769,7 +1140,7 @@
   }).click();
   await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });
 
-  await page.getByRole('button', { name: /Inspect/ }).click();
+  await page.locator('[data-blog-archive]').click();
   await page.waitForFunction(() =>
     document.querySelectorAll('[data-blog-entry]').length === 9
   );
@@ -886,7 +1257,7 @@
   const controlledReload = await page.evaluate(() => {
     const shell = document.querySelector('.jrpg-shell').getBoundingClientRect();
     return {
-      art: getComputedStyle(document.querySelector('.jrpg-scene'))
+      art: getComputedStyle(document.querySelector('.jrpg-workspace'))
         .backgroundImage,
       fillsViewport: shell.left === 0 &&
         shell.top === 0 &&
@@ -899,7 +1270,7 @@
       theme: document.documentElement.dataset.zenTheme,
     };
   });
-  assert.match(controlledReload.art, /bar-ink\.webp/);
+  assert.match(controlledReload.art, /background-frame-2\.webp/);
   assert.equal(controlledReload.fillsViewport, true);
   assert.equal(controlledReload.fontLoaded, true);
   assert.equal(controlledReload.fontFamily, '"Pixel Mplus"');
@@ -939,6 +1310,56 @@
     utilityBelowScene: true,
   });
   await mobileContext.close();
+
+  const shortContext = await browser.newContext({
+    colorScheme: 'dark',
+    viewport: { width: 900, height: 500 },
+  });
+  const shortPage = await shortContext.newPage();
+  await shortPage.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' });
+  const shortLayout = await shortPage.evaluate(() => {
+    const scene = document.querySelector('.jrpg-scene').getBoundingClientRect();
+    const character = document.querySelector('mjj-jrpg-character')
+      .getBoundingClientRect();
+    const chat = document.querySelector('mjj-jrpg-chat')
+      .getBoundingClientRect();
+    return {
+      characterInside: character.left >= scene.left &&
+        character.right <= scene.right,
+      chatInside: chat.left >= scene.left && chat.right <= scene.right,
+      noSceneOverflow: document.querySelector('.jrpg-scene').scrollWidth <=
+        document.querySelector('.jrpg-scene').clientWidth,
+    };
+  });
+  assert.deepEqual(shortLayout, {
+    characterInside: true,
+    chatInside: true,
+    noSceneOverflow: true,
+  });
+  await shortContext.close();
+
+  const forcedColorsContext = await browser.newContext({
+    forcedColors: 'active',
+    viewport: { width: 1280, height: 900 },
+  });
+  const forcedColorsPage = await forcedColorsContext.newPage();
+  await forcedColorsPage.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' });
+  assert.deepEqual(
+    await forcedColorsPage.evaluate(() => ({
+      sceneBar: getComputedStyle(
+        document.querySelector('.jrpg-scene'),
+        '::before',
+      ).backgroundImage,
+      workspace: getComputedStyle(
+        document.querySelector('.jrpg-workspace'),
+      ).backgroundImage,
+    })),
+    {
+      sceneBar: 'none',
+      workspace: 'none',
+    },
+  );
+  await forcedColorsContext.close();
 }
 
 async function testHlsPlayer(browser, siteRoot) {
@@ -1261,7 +1682,7 @@
     for (const source of [home, dogGame, manifest]) {
       assert.doesNotMatch(source, /\.png(?:["')]|$)/i);
     }
-    assert.match(serviceWorker, /v14-blog-browser/);
+    assert.match(serviceWorker, /v30-card-driven-details/);
     assert.match(
       await (
         await fetch(`${baseUrl}/public/pwa-register.js`)