changeset 256:30c2196d03d4

[site] Integrate Zenbu themes and components Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Tue, 04 Aug 2026 16:49:01 -0700
parents 5ec271d612ae
children 609d3c6aff4e
files .claude/skills/zenbu-design-system/SKILL.md .claude/skills/zenbu-personal-site/SKILL.md design_system/README.md design_system/THEMES.md design_system/src/index.html design_system/src/styles/themes.css design_system/src/styles/tokens.css design_system/test/storybook_test.js mrjunejune/BUILD mrjunejune/src/base.css mrjunejune/src/index.html mrjunejune/src/index.js mrjunejune/src/parts/base_head.html mrjunejune/src/parts/header.html mrjunejune/src/public/dog-game.js mrjunejune/src/public/pwa-register.js mrjunejune/src/public/sw.js mrjunejune/src/sw.js mrjunejune/src/tools/hls_player/index.css mrjunejune/src/tools/markdown_to_html/index.css mrjunejune/test/snapshots/blog.snapshot mrjunejune/test/snapshots/resume.snapshot mrjunejune/test/snapshots/root.snapshot mrjunejune/test/snapshots/talk.snapshot mrjunejune/test/snapshots/tools.snapshot mrjunejune/test/snapshots/tools_file_converter.snapshot mrjunejune/test/snapshots/tools_file_converter_index.html.snapshot mrjunejune/test/snapshots/tools_hls_player.snapshot mrjunejune/test/snapshots/tools_hls_player_index.html.snapshot mrjunejune/test/snapshots/tools_latex_editor.snapshot mrjunejune/test/snapshots/tools_latex_editor_index.html.snapshot mrjunejune/test/snapshots/tools_markdown_to_html.snapshot mrjunejune/test/snapshots/tools_markdown_to_html_index.html.snapshot mrjunejune/test/theme_and_webp_test.js
diffstat 32 files changed, 1244 insertions(+), 1165 deletions(-) [+]
line wrap: on
line diff
--- a/.claude/skills/zenbu-design-system/SKILL.md	Tue Aug 04 15:12:18 2026 -0700
+++ b/.claude/skills/zenbu-design-system/SKILL.md	Tue Aug 04 16:49:01 2026 -0700
@@ -40,6 +40,9 @@
   widths and reject horizontal page overflow.
 - Preserve the warm, restrained MUJI-like visual character: paper neutrals,
   natural material colors, strong typography, and deliberate color accents.
+- Treat themes as semantic-token compositions. Preserve the built-in Paper,
+  Ink, and Playful themes plus automatic system light/dark behavior; components
+  must not branch on theme names.
 - Use semantic `--zen-color-*` aliases in component CSS. Use primitive ramps
   or the ten data tokens only when a semantic role does not fit; never encode
   meaning through color alone.
--- a/.claude/skills/zenbu-personal-site/SKILL.md	Tue Aug 04 15:12:18 2026 -0700
+++ b/.claude/skills/zenbu-personal-site/SKILL.md	Tue Aug 04 16:49:01 2026 -0700
@@ -13,6 +13,9 @@
 - `mrjunejune/src/`: static site source files and HTML templates.
 - `mrjunejune/src/parts/`: shared HTML includes used by `{{...}}` template placeholders.
 - `mrjunejune/src/public/`: public assets, PWA files, media, generated JS, icons, resume PDF.
+- Zenbu UI assets are copied from `//design_system:components` and
+  `//design_system:styles` into `src/public/design-system/` by Bazel. Do not
+  hand-copy or fork shared component files.
 - `mrjunejune/test/`: integration tests and snapshots.
 - `mrjunejune/BUILD`: Bazel build, bundle, asset movement, and test-visible filegroups.
 - `mrjunejune/.config` and `.config.development`: server configuration. Do not commit secrets.
@@ -68,4 +71,7 @@
 - For binary responses, set `content-length` and avoid string-only operations on body bytes.
 - If a route is part of the public website, add or update snapshot coverage in `mrjunejune/test`.
 - Keep PWA changes consistent across `manifest.json`, `sw.js`, icons, and `PWA_SETUP.md`.
+- Site themes use `data-zen-theme` with Auto, Paper, Ink, and Playful states.
+  Preserve the More Sugar font aliases and keep theme-specific values in shared
+  design-system tokens rather than branching inside components.
 - Reuse `seobeo`, `dowa`, `deita`, `s3`, and `markdown_converter` instead of adding parallel implementations.
--- a/design_system/README.md	Tue Aug 04 15:12:18 2026 -0700
+++ b/design_system/README.md	Tue Aug 04 16:49:01 2026 -0700
@@ -57,6 +57,8 @@
 Every visual token is a `--zen-*` custom property and can be overridden by the
 application. See [`COLORS.md`](COLORS.md) for the complete warm color ramps,
 material aliases, semantic roles, dark-theme behavior, and data palette.
+Named [`themes`](THEMES.md) compose those tokens as Paper, Ink, and Playful
+experiences in addition to automatic system light/dark behavior.
 
 Use [`zen-icon`](ICONS.md) for all interface iconography. Design-system source
 does not use emoji, ad hoc SVG, icon fonts, or third-party icon packages.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/design_system/THEMES.md	Tue Aug 04 16:49:01 2026 -0700
@@ -0,0 +1,28 @@
+# Zenbu UI themes
+
+Themes are named token compositions, not separate component stylesheets.
+Applications select one with `data-zen-theme` on the document root:
+
+```html
+<html data-zen-theme="paper">
+```
+
+Built-in themes:
+
+- `paper`: warm neutral surfaces, restrained natural accents, editorial type;
+- `ink`: dark charcoal surfaces with paper-like text and softer status colors;
+- `playful`: brighter clay, persimmon, violet, and rounded shapes;
+- no attribute: follow the operating-system light/dark preference.
+
+Applications can provide their own fonts without changing component CSS:
+
+```css
+:root {
+  --zen-font-editorial: "More Thin", sans-serif;
+  --zen-font-playful: "More", sans-serif;
+}
+```
+
+Import `themes.css` after `tokens.css` and before component CSS. Components
+continue to consume semantic tokens, so every built-in and application theme
+inherits the same accessibility, motion, and native-HTML behavior.
--- a/design_system/src/index.html	Tue Aug 04 15:12:18 2026 -0700
+++ b/design_system/src/index.html	Tue Aug 04 16:49:01 2026 -0700
@@ -6,6 +6,7 @@
     <meta name="color-scheme" content="light dark" />
     <title>Zenbu UI</title>
     <link rel="stylesheet" href="/styles/tokens.css" />
+    <link rel="stylesheet" href="/styles/themes.css" />
     <link rel="stylesheet" href="/styles/components.css" />
     <link rel="stylesheet" href="/storybook.css" />
     <script type="module" src="/storybook.js"></script>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/design_system/src/styles/themes.css	Tue Aug 04 16:49:01 2026 -0700
@@ -0,0 +1,95 @@
+:root[data-zen-theme="paper"] {
+  color-scheme: light;
+  --zen-color-canvas: var(--zen-color-neutral-100);
+  --zen-color-surface: var(--zen-color-neutral-50);
+  --zen-color-surface-raised: var(--zen-color-neutral-50);
+  --zen-color-surface-muted: var(--zen-color-neutral-100);
+  --zen-color-surface-sunken: var(--zen-color-neutral-200);
+  --zen-color-text: var(--zen-color-neutral-950);
+  --zen-color-text-muted: var(--zen-color-neutral-600);
+  --zen-color-text-subtle: var(--zen-color-neutral-500);
+  --zen-color-border: var(--zen-color-neutral-800);
+  --zen-color-border-muted: var(--zen-color-neutral-300);
+  --zen-color-accent: var(--zen-color-amber-400);
+  --zen-color-accent-hover: var(--zen-color-amber-300);
+  --zen-color-accent-muted: var(--zen-color-amber-100);
+  --zen-color-info: var(--zen-color-blue-600);
+  --zen-color-success: var(--zen-color-green-600);
+  --zen-color-warning: var(--zen-color-orange-600);
+  --zen-color-danger: var(--zen-color-red-600);
+  --zen-color-focus: var(--zen-color-violet-600);
+  --zen-font-sans: var(
+    --zen-font-editorial,
+    ui-rounded,
+    system-ui,
+    sans-serif
+  );
+}
+
+:root[data-zen-theme="ink"] {
+  color-scheme: dark;
+  --zen-color-canvas: var(--zen-color-neutral-950);
+  --zen-color-surface: var(--zen-color-neutral-900);
+  --zen-color-surface-raised: var(--zen-color-neutral-800);
+  --zen-color-surface-muted: var(--zen-color-neutral-800);
+  --zen-color-surface-sunken: var(--zen-color-neutral-950);
+  --zen-color-text: var(--zen-color-neutral-50);
+  --zen-color-text-muted: var(--zen-color-neutral-300);
+  --zen-color-text-subtle: var(--zen-color-neutral-400);
+  --zen-color-border: var(--zen-color-neutral-200);
+  --zen-color-border-muted: var(--zen-color-neutral-700);
+  --zen-color-accent: var(--zen-color-amber-300);
+  --zen-color-accent-hover: var(--zen-color-amber-200);
+  --zen-color-accent-muted: var(--zen-color-amber-800);
+  --zen-color-info: var(--zen-color-blue-300);
+  --zen-color-success: var(--zen-color-green-300);
+  --zen-color-warning: var(--zen-color-orange-300);
+  --zen-color-danger: var(--zen-color-red-300);
+  --zen-color-on-info: var(--zen-color-neutral-950);
+  --zen-color-on-success: var(--zen-color-neutral-950);
+  --zen-color-on-warning: var(--zen-color-neutral-950);
+  --zen-color-on-danger: var(--zen-color-neutral-950);
+  --zen-color-focus: var(--zen-color-violet-300);
+  --zen-color-on-accent: var(--zen-color-neutral-950);
+  --zen-color-text-inverse: var(--zen-color-neutral-950);
+  --zen-font-sans: var(
+    --zen-font-editorial,
+    ui-rounded,
+    system-ui,
+    sans-serif
+  );
+}
+
+:root[data-zen-theme="playful"] {
+  color-scheme: light;
+  --zen-color-canvas: var(--zen-color-rose-100);
+  --zen-color-surface: var(--zen-color-amber-50);
+  --zen-color-surface-raised: var(--zen-color-neutral-50);
+  --zen-color-surface-muted: var(--zen-color-orange-100);
+  --zen-color-surface-sunken: var(--zen-color-violet-100);
+  --zen-color-text: var(--zen-color-blue-900);
+  --zen-color-text-muted: var(--zen-color-violet-700);
+  --zen-color-text-subtle: var(--zen-color-blue-600);
+  --zen-color-border: var(--zen-color-violet-700);
+  --zen-color-border-muted: var(--zen-color-rose-300);
+  --zen-color-accent: var(--zen-color-orange-300);
+  --zen-color-accent-hover: var(--zen-color-amber-300);
+  --zen-color-accent-muted: var(--zen-color-orange-100);
+  --zen-color-info: var(--zen-color-blue-600);
+  --zen-color-success: var(--zen-color-green-600);
+  --zen-color-warning: var(--zen-color-amber-600);
+  --zen-color-danger: var(--zen-color-rose-600);
+  --zen-color-on-success: var(--zen-color-neutral-50);
+  --zen-color-on-warning: var(--zen-color-neutral-50);
+  --zen-color-focus: var(--zen-color-violet-500);
+  --zen-color-on-accent: var(--zen-color-blue-950);
+  --zen-font-sans: var(
+    --zen-font-playful,
+    ui-rounded,
+    system-ui,
+    sans-serif
+  );
+  --zen-radius-sm: 0.65rem;
+  --zen-radius-md: 1rem;
+  --zen-radius-lg: 1.5rem;
+}
--- a/design_system/src/styles/tokens.css	Tue Aug 04 15:12:18 2026 -0700
+++ b/design_system/src/styles/tokens.css	Tue Aug 04 16:49:01 2026 -0700
@@ -268,7 +268,7 @@
 }
 
 @media (prefers-color-scheme: dark) {
-  :root:not([data-zen-theme="light"]) {
+  :root:not([data-zen-theme]) {
     color-scheme: dark;
     --zen-color-canvas: #101218;
     --zen-color-surface: #181b23;
--- a/design_system/test/storybook_test.js	Tue Aug 04 15:12:18 2026 -0700
+++ b/design_system/test/storybook_test.js	Tue Aug 04 16:49:01 2026 -0700
@@ -138,6 +138,7 @@
     }
     for (const asset of [
       '/styles/tokens.css',
+      '/styles/themes.css',
       '/styles/components.css',
       '/styles/shadcn.css',
       '/components/index.js',
@@ -480,6 +481,52 @@
         { length: 10 },
         (_, index) => value(`data-${index + 1}`),
       );
+      const themes = ['paper', 'ink', 'playful'].map(theme => {
+        root.dataset.zenTheme = theme;
+        const channels = value => value.match(/\d+(?:\.\d+)?/g)
+          .slice(0, 3)
+          .map(Number);
+        const luminance = value => {
+          const converted = channels(value).map(channel => {
+            const normalized = channel / 255;
+            return normalized <= 0.04045
+              ? normalized / 12.92
+              : ((normalized + 0.055) / 1.055) ** 2.4;
+          });
+          return converted[0] * 0.2126 +
+            converted[1] * 0.7152 +
+            converted[2] * 0.0722;
+        };
+        const contrasts = [
+          'info',
+          'success',
+          'warning',
+          'danger',
+        ].map(tone => {
+          const probe = document.createElement('span');
+          probe.style.background = `var(--zen-color-${tone})`;
+          probe.style.color = `var(--zen-color-on-${tone})`;
+          document.body.append(probe);
+          const probeStyle = getComputedStyle(probe);
+          const foreground = luminance(probeStyle.color);
+          const background = luminance(probeStyle.backgroundColor);
+          const contrast = (Math.max(foreground, background) + 0.05) /
+            (Math.min(foreground, background) + 0.05);
+          probe.remove();
+          return contrast;
+        });
+        return {
+          canvas: value('canvas'),
+          contrast: Math.min(...contrasts),
+          font: getComputedStyle(root)
+            .getPropertyValue('--zen-font-sans')
+            .trim(),
+          name: theme,
+          radius: getComputedStyle(root)
+            .getPropertyValue('--zen-radius-lg')
+            .trim(),
+        };
+      });
       delete root.dataset.zenTheme;
       return {
         darkData,
@@ -488,6 +535,7 @@
         lightMuted,
         materials,
         rampsComplete,
+        themes,
       };
     });
     assert.equal(colorTokenState.rampsComplete, true);
@@ -495,6 +543,18 @@
     assert.equal(new Set(colorTokenState.data).size, 10);
     assert.equal(new Set(colorTokenState.darkData).size, 10);
     assert.notDeepEqual(colorTokenState.data, colorTokenState.darkData);
+    assert.equal(
+      new Set(colorTokenState.themes.map(theme => theme.canvas)).size,
+      3,
+    );
+    assert.notEqual(
+      colorTokenState.themes.find(theme => theme.name === 'paper').radius,
+      colorTokenState.themes.find(theme => theme.name === 'playful').radius,
+    );
+    assert.ok(
+      colorTokenState.themes.every(theme => theme.contrast >= 4.5),
+      JSON.stringify(colorTokenState.themes),
+    );
     assert.notEqual(
       colorTokenState.lightMuted,
       colorTokenState.darkMuted,
--- a/mrjunejune/BUILD	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/BUILD	Tue Aug 04 16:49:01 2026 -0700
@@ -134,6 +134,18 @@
 )
 
 move_files_into_dir(
+  name = "design_system_components",
+  srcs = ["//design_system:components"],
+  dest = "src/public/design-system/components",
+)
+
+move_files_into_dir(
+  name = "design_system_styles",
+  srcs = ["//design_system:styles"],
+  dest = "src/public/design-system/styles",
+)
+
+move_files_into_dir(
   name = "icons",
   srcs = [
     "//assets:icons",
@@ -161,7 +173,7 @@
   srcs = glob(
     ["src/**"],
     exclude = ["src/**/*.png"],
-  ) + [":react_pages", ":shared_js_non_public", ":shared_js_file", ":rich_editor_js", ":icons", ":generated_public_webp", ":hlsjs_runtime"],
+  ) + [":react_pages", ":shared_js_non_public", ":shared_js_file", ":rich_editor_js", ":design_system_components", ":design_system_styles", ":icons", ":generated_public_webp", ":hlsjs_runtime"],
   visibility = ["//mrjunejune/test:__pkg__"],
 )
 
--- a/mrjunejune/src/base.css	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/src/base.css	Tue Aug 04 16:49:01 2026 -0700
@@ -74,7 +74,7 @@
     0 -8px -24px rgba(159, 140, 96, 33%), 0 -16px -32px rgba(159, 140, 96, 33%);
 }
 @media (prefers-color-scheme: dark) {
-  :root:not(.light-mode) {
+  :root:not([data-zen-theme]) {
     --white: hsl(224, 10%, 10%);
     --black: hsl(0, 0%, 90%);
     --darkgray: #cccccc;
@@ -98,6 +98,39 @@
   }
 }
 
+:root {
+  --zen-font-editorial: "More Thin", "Roboto", sans-serif;
+  --zen-font-playful: "More", "More Thin", sans-serif;
+  --zen-font-sans: var(--zen-font-editorial);
+}
+
+:root:not([data-zen-theme]),
+:root[data-zen-theme] {
+  --white: var(--zen-color-surface);
+  --black: var(--zen-color-text);
+  --darkgray: var(--zen-color-border-strong);
+  --lightgray: var(--zen-color-border-muted);
+  --gray-light: var(--zen-color-border-muted);
+  --green: var(--zen-color-success);
+  --orange: var(--zen-color-warning);
+  --purple: var(--zen-color-violet-400);
+  --red: var(--zen-color-danger);
+  --blue: var(--zen-color-info);
+  --darktext: var(--zen-color-text-muted);
+  --text: var(--zen-color-text);
+  --graytext: var(--zen-color-text-muted);
+  --lighttext: var(--zen-color-text-subtle);
+  --awesome: var(--zen-color-persimmon);
+  --accent: var(--zen-color-info);
+  --accent-dark: var(--zen-color-indigo);
+  --gray-dark: var(--zen-color-text);
+  --gray-gradient: linear-gradient(
+    var(--zen-color-canvas),
+    var(--zen-color-surface)
+  );
+  --box-shadow: var(--zen-shadow-md);
+}
+
 /* --- fonts --- */
 /* 
 @font-face {
@@ -141,7 +174,7 @@
 }
 
 body {
-  font-family: "More Thin", sans-serif;
+  font-family: var(--zen-font-sans, "More Thin", sans-serif);
   margin: 0;
   padding: 0;
   text-align: left;
@@ -149,7 +182,7 @@
   background-size: 100% 600px;
   word-wrap: break-word;
   overflow-wrap: break-word;
-  color: rgb(var(--gray-dark));
+  color: var(--gray-dark);
   font-size: 20px;
   line-height: 1.7;
   -webkit-font-smoothing: antialiased;
@@ -170,7 +203,7 @@
 h5,
 h6 {
   margin: 0 0 0.5rem 0;
-  color: rgb(var(--black));
+  color: var(--black);
   line-height: 1.2;
 }
 
@@ -228,7 +261,7 @@
 
 code {
   padding: 2px 5px;
-  background-color: rgb(var(--gray-light));
+  background-color: var(--gray-light);
   border-radius: 2px;
 }
 
@@ -250,7 +283,7 @@
 
 hr {
   border: none;
-  border-top: 1px solid rgb(var(--gray-light));
+  border-top: 1px solid var(--gray-light);
 }
 
 .center {
@@ -395,4 +428,3 @@
   /* Icon color = button text color */
   background-color: currentColor;
 }
-
--- a/mrjunejune/src/index.html	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/src/index.html	Tue Aug 04 16:49:01 2026 -0700
@@ -64,6 +64,28 @@
         z-index: 101;
         cursor: pointer;
       }
+
+      .site-link-grid {
+        display: grid;
+        grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
+        gap: var(--zen-space-4);
+      }
+
+      .site-link-grid > a {
+        color: inherit;
+        text-decoration: none;
+      }
+
+      .site-link-grid zen-card,
+      .site-link-grid article {
+        height: 100%;
+      }
+
+      .site-link-grid p {
+        margin: 0;
+        color: var(--zen-color-text-muted);
+        font-size: 0.9rem;
+      }
     </style>
   </head>
   <body>
@@ -80,12 +102,28 @@
        </div>
 
        <h2>Links</h2>
-       <ul>
-         <li><a href="https://zenbu.babocoder.com/file/tip">Repository</a> - Check out my code</li>
-         <li><a href="/blog">Blogs</a> - My thoughts / Experiments </li>
-         <li><a href="/resume">Resume</a> - My professional experiences </li>
-         <li><a href="/tools">Tools</a> - Things I made for myself </li>
-       </ul> 
+       <div class="site-link-grid">
+         <a href="https://zenbu.babocoder.com/file/tip">
+           <zen-card interactive>
+             <article><h3>Repository</h3><p>Browse the code behind these experiments.</p></article>
+           </zen-card>
+         </a>
+         <a href="/blog">
+           <zen-card interactive>
+             <article><h3>Blogs</h3><p>Thoughts, experiments, and technical notes.</p></article>
+           </zen-card>
+         </a>
+         <a href="/resume">
+           <zen-card interactive>
+             <article><h3>Resume</h3><p>Professional experience and selected work.</p></article>
+           </zen-card>
+         </a>
+         <a href="/tools">
+           <zen-card interactive>
+             <article><h3>Tools</h3><p>Small things I built for myself.</p></article>
+           </zen-card>
+         </a>
+       </div>
        <canvas id="background"></canvas>
        <canvas id="game"></canvas>
        <canvas id="gameUI"></canvas>
--- a/mrjunejune/src/index.js	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/src/index.js	Tue Aug 04 16:49:01 2026 -0700
@@ -1,50 +1,74 @@
 const THEME_STORAGE_KEY = 'theme-preference';
-// Get stored preference or detect system preference
-function getThemePreference()
-{
-  const stored = localStorage.getItem(THEME_STORAGE_KEY);
-  if (stored) {
-    return stored;
-  }
-  return 'auto';
+const THEMES = ['auto', 'paper', 'ink', 'playful'];
+const THEME_LABELS = {
+  auto: 'Auto',
+  paper: 'Paper',
+  ink: 'Ink',
+  playful: 'Playful',
+};
+
+function normalizeTheme(value) {
+  if (value === 'light') return 'paper';
+  if (value === 'dark') return 'ink';
+  return THEMES.includes(value) ? value : 'auto';
+}
+
+function getThemePreference() {
+  return normalizeTheme(localStorage.getItem(THEME_STORAGE_KEY));
+}
+
+function updateThemeColor() {
+  const probe = document.createElement('span');
+  probe.style.background = 'var(--zen-color-canvas)';
+  document.body.append(probe);
+  const color = getComputedStyle(probe).backgroundColor;
+  probe.remove();
+  document.querySelector('meta[name="theme-color"]')
+    ?.setAttribute('content', color);
 }
 
-function applyTheme(preference)
-{
-  const root = document.documentElement;
-
-  if (preference === 'light') {
-    root.classList.remove('dark', 'auto');
-    root.classList.add('light-mode');
-  } else if (preference === 'dark') {
-    root.classList.remove('light-mode', 'auto');
-    root.classList.add('dark');
-  } else {
-    // Auto - remove manual overrides and let CSS media query handle it
-    root.classList.remove('light-mode', 'dark');
-    root.classList.add('auto');
-  }
+function updateThemeControl(preference) {
+  const toggle = document.querySelector('#themeToggle');
+  const label = document.querySelector('#themeName');
+  if (label) label.textContent = THEME_LABELS[preference];
+  toggle?.setAttribute(
+    'aria-label',
+    `Change site theme. Current theme: ${THEME_LABELS[preference]}`,
+  );
 }
 
-(function() {
-  const currentPreference = getThemePreference();
-  applyTheme(currentPreference);
-})();
-
-/* Toggle lights auto and dark */
-const toggle = document.querySelector('#themeToggle');
-toggle.addEventListener('click', function() {
-  let preference = getThemePreference();
+function applyTheme(value, persist = false) {
+  const preference = normalizeTheme(value);
+  const root = document.documentElement;
+  root.classList.remove('light-mode', 'dark', 'auto');
+  if (preference === 'auto') delete root.dataset.zenTheme;
+  else root.dataset.zenTheme = preference;
+  if (persist) localStorage.setItem(THEME_STORAGE_KEY, preference);
+  updateThemeControl(preference);
+  requestAnimationFrame(updateThemeColor);
+  document.dispatchEvent(new CustomEvent('zen-theme-change', {
+    detail: { theme: preference },
+  }));
+  return preference;
+}
 
-  // Cycle through: auto -> light -> dark -> auto
-  if (preference === 'auto') {
-    preference = 'light';
-  } else if (preference === 'light') {
-    preference = 'dark';
-  } else {
-    preference = 'auto';
-  }
+const currentPreference = applyTheme(getThemePreference());
+if (localStorage.getItem(THEME_STORAGE_KEY) !== currentPreference) {
+  localStorage.setItem(THEME_STORAGE_KEY, currentPreference);
+}
 
-  localStorage.setItem(THEME_STORAGE_KEY, preference);
-  applyTheme(preference);
+document.querySelector('#themeToggle')?.addEventListener('click', () => {
+  const current = getThemePreference();
+  const next = THEMES[(THEMES.indexOf(current) + 1) % THEMES.length];
+  applyTheme(next, true);
 });
+
+matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
+  if (getThemePreference() === 'auto') updateThemeColor();
+});
+
+window.MrJuneJuneTheme = Object.freeze({
+  apply: theme => applyTheme(theme, true),
+  current: getThemePreference,
+  themes: [...THEMES],
+});
--- a/mrjunejune/src/parts/base_head.html	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/src/parts/base_head.html	Tue Aug 04 16:49:01 2026 -0700
@@ -1,6 +1,6 @@
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
-<meta name="theme-color" content="#544e43">
+<meta name="theme-color" content="#f7f3e8">
 <link rel="icon" type="image/svg+xml" href="/public/epi_all_colors.svg">
 <link rel="manifest" href="/public/manifest.json">
 <link rel="apple-touch-icon" href="/public/epi_all_colors.svg">
@@ -16,8 +16,14 @@
 <link rel="preload" href="/public/fonts/more-sugar.thin.otf" as="font" type="font/otf" crossorigin>
 <link rel="preload" href="/public/epi_all_colors.svg" as="image">
 
+<link rel="stylesheet" href="/public/design-system/styles/tokens.css" />
+<link rel="stylesheet" href="/public/design-system/styles/themes.css" />
 <link rel="preload" href="/base.css" as="style" />
 <link rel="stylesheet" href="/base.css" />
+<link rel="stylesheet" href="/public/design-system/styles/components.css" />
+<script
+  type="module"
+  src="/public/design-system/components/index.js"
+></script>
 
 <script src="/public/pwa-register.js" defer></script>
-
--- a/mrjunejune/src/parts/header.html	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/src/parts/header.html	Tue Aug 04 16:49:01 2026 -0700
@@ -1,35 +1,41 @@
 <style>
   :root {
     --header-background: var(--white);
-    --header-color: rgb(var(--black));
+    --header-color: var(--black);
     --link-hover-accent: var(--awesome);
   }
 
-  /* Fixed icon in top left corner */
-  #themeToggle {
+  #themeControl {
     position: fixed;
     top: 20px;
     left: 20px;
-    background: var(--header-background);
-    display: flex;
-    align-items: center;
-    border-radius: 50%;
-    cursor: pointer;
     z-index: 1000;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
     transition: transform 0.2s ease;
   }
 
-  #themeToggle:hover {
+  #themeControl:hover {
     transform: scale(1.05);
   }
 
+  #themeToggle {
+    flex-direction: column;
+    min-width: 4.5rem;
+    padding: 0.35rem;
+    border-radius: 1.5rem;
+  }
+
+  #themeName {
+    font-family: "More", sans-serif;
+    font-size: 0.65rem;
+    line-height: 1;
+  }
+
   /* Professional header */
   header {
     margin: auto;
     padding: 1.5em 1em;
     font-family: "More", sans-serif;
-    box-shadow: 0 2px 8px rgba(var(--black), 5%);
+    box-shadow: var(--zen-shadow-sm);
     width: 720px;
     max-width: calc(100% - 2em);
     text-align: center;
@@ -53,7 +59,7 @@
 
   /* Mobile responsiveness */
   @media (max-width: 720px) {
-    #themeToggle {
+    #themeControl {
       top: 15px;
       left: 15px;
     }
@@ -68,7 +74,7 @@
   }
 
   @media (max-width: 480px) {
-    #themeToggle {
+    #themeControl {
       top: 10px;
       left: 10px;
     }
@@ -94,20 +100,25 @@
   }
   
   /* 2. MANUAL DARK OVERRIDE */
-  html.dark {
+  html[data-zen-theme="ink"] {
     --logo-invert: invert(1);
     --epi-grayscale: grayscale(1);
   }
   
   /* 3. MANUAL LIGHT OVERRIDE */
-  html.light-mode {
+  html[data-zen-theme="paper"] {
     --logo-invert: invert(0);
     --epi-grayscale: brightness(2.9) grayscale(1);
   }
+
+  html[data-zen-theme="playful"] {
+    --logo-invert: invert(0);
+    --epi-grayscale: saturate(1.25);
+  }
   
   /* 4. SYSTEM PREFERENCE */
   @media (prefers-color-scheme: dark) {
-    :root:not(.light-mode) {
+    :root:not([data-zen-theme]) {
       --logo-invert: invert(1);
     }
   }
@@ -126,12 +137,25 @@
   }
 </style>
 
-<div id="themeToggle">
-  <img id="epiChan" class="epi-logo" aria-label="Toggle dark mode" src="/public/epi_all_colors.svg" height="50" width="50">
-</div>
+<zen-button id="themeControl" variant="quiet">
+  <button
+    id="themeToggle"
+    type="button"
+    aria-label="Change site theme"
+  >
+    <img
+      id="epiChan"
+      class="epi-logo"
+      alt=""
+      src="/public/epi_all_colors.svg"
+      height="50"
+      width="50"
+    >
+    <span id="themeName" aria-live="polite">Auto</span>
+  </button>
+</zen-button>
 
 <header>
   <h1><a href="/">MrJuneJune</a></h1>
 </header>
 <script src="/index.js"></script>
-
--- a/mrjunejune/src/public/dog-game.js	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/src/public/dog-game.js	Tue Aug 04 16:49:01 2026 -0700
@@ -51,8 +51,8 @@
 
 function isDarkMode() {
   const root = document.documentElement;
-  if (root.classList.contains("dark")) return true;
-  if (root.classList.contains("light-mode")) return false;
+  if (root.dataset.zenTheme === "ink") return true;
+  if (root.dataset.zenTheme) return false;
   return darkModePreference.matches;
 }
 
@@ -324,8 +324,9 @@
 window.addEventListener('resize', resizeCanvas);
 new MutationObserver(updateStarPalette).observe(
   document.documentElement,
-  { attributes: true, attributeFilter: ["class"] }
+  { attributes: true, attributeFilter: ["data-zen-theme"] }
 );
+document.addEventListener("zen-theme-change", updateStarPalette);
 darkModePreference.addEventListener("change", updateStarPalette);
 
 window.onload = () => {
--- a/mrjunejune/src/public/pwa-register.js	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/src/public/pwa-register.js	Tue Aug 04 16:49:01 2026 -0700
@@ -2,7 +2,7 @@
 if ('serviceWorker' in navigator) {
   window.addEventListener('load', () => {
     navigator.serviceWorker
-      .register('/public/sw.js')
+      .register('/sw.js', { scope: '/' })
       .then((registration) => {
         // console.log('[PWA] Service Worker registered:', registration.scope);
 
--- a/mrjunejune/src/public/sw.js	Tue Aug 04 15:12:18 2026 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,143 +0,0 @@
-// Service Worker for MrJuneJune PWA
-const CACHE_VERSION = 'v4-hlsjs';
-const CACHE_NAME = `mrjunejune-${CACHE_VERSION}`;
-
-// Files to cache immediately on install
-const STATIC_CACHE = [
-  '/',
-  '/base.css',
-  '/public/epi_all_colors.svg',
-  '/public/fonts/Roboto-Regular.ttf',
-  '/public/fonts/Roboto-Thin.ttf',
-  '/public/fonts/more-sugar.regular.otf',
-  '/public/fonts/more-sugar.thin.otf',
-];
-
-// Install event - cache static assets
-self.addEventListener('install', (event) => {
-  console.log('[SW] Installing service worker...');
-
-  event.waitUntil(
-    caches.open(CACHE_NAME).then((cache) => {
-      console.log('[SW] Caching static assets');
-      return cache.addAll(STATIC_CACHE);
-    }).then(() => {
-      console.log('[SW] Skip waiting');
-      return self.skipWaiting();
-    })
-  );
-});
-
-// Activate event - clean up old caches
-self.addEventListener('activate', (event) => {
-  console.log('[SW] Activating service worker...');
-
-  event.waitUntil(
-    caches.keys().then((cacheNames) => {
-      return Promise.all(
-        cacheNames.map((cacheName) => {
-          if (cacheName !== CACHE_NAME) {
-            console.log('[SW] Deleting old cache:', cacheName);
-            return caches.delete(cacheName);
-          }
-        })
-      );
-    }).then(() => {
-      console.log('[SW] Claiming clients');
-      return self.clients.claim();
-    })
-  );
-});
-
-// Fetch event - serve from cache, fallback to network
-self.addEventListener('fetch', (event) => {
-  const { request } = event;
-  const url = new URL(request.url);
-
-  // Skip non-GET requests
-  if (request.method !== 'GET') {
-    return;
-  }
-
-  // Skip chrome-extension and other non-http(s) requests
-  if (!url.protocol.startsWith('http')) {
-    return;
-  }
-
-  // Skip API calls and media uploads (always go to network)
-  if (url.pathname.startsWith('/api/')) {
-    return;
-  }
-
-  // The HLS tool and media must always reflect the current player version.
-  if (url.pathname.startsWith('/tools/hls_player') ||
-      url.pathname.startsWith('/public/hls-sample/')) {
-    return;
-  }
-
-  event.respondWith(
-    caches.match(request).then((cachedResponse) => {
-      if (cachedResponse) {
-        console.log('[SW] Serving from cache:', url.pathname);
-        return cachedResponse;
-      }
-
-      // Not in cache, fetch from network
-      return fetch(request).then((networkResponse) => {
-        // Only cache successful responses
-        if (!networkResponse || networkResponse.status !== 200 || networkResponse.type === 'error') {
-          return networkResponse;
-        }
-
-        // Cache specific file types
-        const shouldCache =
-          url.pathname.endsWith('.css') ||
-          url.pathname.endsWith('.js') ||
-          url.pathname.endsWith('.svg') ||
-          url.pathname.endsWith('.jpg') ||
-          url.pathname.endsWith('.webp') ||
-          url.pathname.endsWith('.woff') ||
-          url.pathname.endsWith('.woff2') ||
-          url.pathname.endsWith('.ttf') ||
-          url.pathname.endsWith('.otf') ||
-          url.pathname.startsWith('/blog/') ||
-          url.pathname.startsWith('/notes/') ||
-          url.pathname === '/';
-
-        if (shouldCache) {
-          const responseToCache = networkResponse.clone();
-          caches.open(CACHE_NAME).then((cache) => {
-            console.log('[SW] Caching new resource:', url.pathname);
-            cache.put(request, responseToCache);
-          });
-        }
-
-        return networkResponse;
-      }).catch((error) => {
-        console.log('[SW] Fetch failed:', error);
-
-        // Return offline page for HTML requests
-        if (request.headers.get('Accept').includes('text/html')) {
-          return caches.match('/offline.html');
-        }
-      });
-    })
-  );
-});
-
-// Handle messages from the client
-self.addEventListener('message', (event) => {
-  if (event.data && event.data.type === 'SKIP_WAITING') {
-    self.skipWaiting();
-  }
-
-  if (event.data && event.data.type === 'CLEAR_CACHE') {
-    event.waitUntil(
-      caches.keys().then((cacheNames) => {
-        return Promise.all(
-          cacheNames.map((cacheName) => caches.delete(cacheName))
-        );
-      })
-    );
-  }
-});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/sw.js	Tue Aug 04 16:49:01 2026 -0700
@@ -0,0 +1,162 @@
+// Root-scoped Service Worker for MrJuneJune PWA
+const CACHE_VERSION = 'v5-zenbu-themes';
+const CACHE_NAME = `mrjunejune-${CACHE_VERSION}`;
+
+// Files to cache immediately on install
+const STATIC_CACHE = [
+  '/',
+  '/index.js',
+  '/base.css',
+  '/public/design-system/styles/tokens.css',
+  '/public/design-system/styles/themes.css',
+  '/public/design-system/styles/components.css',
+  '/public/design-system/styles/shadcn.css',
+  '/public/design-system/components/index.js',
+  '/public/design-system/components/alert.js',
+  '/public/design-system/components/button.js',
+  '/public/design-system/components/card.js',
+  '/public/design-system/components/collections.js',
+  '/public/design-system/components/disclosure.js',
+  '/public/design-system/components/field.js',
+  '/public/design-system/components/forms.js',
+  '/public/design-system/components/icon.js',
+  '/public/design-system/components/notifications.js',
+  '/public/design-system/components/overlays.js',
+  '/public/design-system/components/primitives.js',
+  '/public/design-system/components/stack.js',
+  '/public/design-system/components/story.js',
+  '/public/epi_all_colors.svg',
+  '/public/fonts/Roboto-Regular.ttf',
+  '/public/fonts/Roboto-Thin.ttf',
+  '/public/fonts/more-sugar.regular.otf',
+  '/public/fonts/more-sugar.thin.otf',
+];
+
+// Install event - cache static assets
+self.addEventListener('install', (event) => {
+  console.log('[SW] Installing service worker...');
+
+  event.waitUntil(
+    caches.open(CACHE_NAME).then((cache) => {
+      console.log('[SW] Caching static assets');
+      return cache.addAll(STATIC_CACHE);
+    }).then(() => {
+      console.log('[SW] Skip waiting');
+      return self.skipWaiting();
+    })
+  );
+});
+
+// Activate event - clean up old caches
+self.addEventListener('activate', (event) => {
+  console.log('[SW] Activating service worker...');
+
+  event.waitUntil(
+    caches.keys().then((cacheNames) => {
+      return Promise.all(
+        cacheNames.map((cacheName) => {
+          if (cacheName !== CACHE_NAME) {
+            console.log('[SW] Deleting old cache:', cacheName);
+            return caches.delete(cacheName);
+          }
+        })
+      );
+    }).then(() => {
+      console.log('[SW] Claiming clients');
+      return self.clients.claim();
+    })
+  );
+});
+
+// Fetch event - serve from cache, fallback to network
+self.addEventListener('fetch', (event) => {
+  const { request } = event;
+  const url = new URL(request.url);
+
+  // Skip non-GET requests
+  if (request.method !== 'GET') {
+    return;
+  }
+
+  // Skip chrome-extension and other non-http(s) requests
+  if (!url.protocol.startsWith('http')) {
+    return;
+  }
+
+  // Skip API calls and media uploads (always go to network)
+  if (url.pathname.startsWith('/api/')) {
+    return;
+  }
+
+  // The HLS tool and media must always reflect the current player version.
+  if (url.pathname.startsWith('/tools/hls_player') ||
+      url.pathname.startsWith('/public/hls-sample/')) {
+    return;
+  }
+
+  event.respondWith(
+    caches.match(request).then((cachedResponse) => {
+      if (cachedResponse) {
+        console.log('[SW] Serving from cache:', url.pathname);
+        return cachedResponse;
+      }
+
+      // Not in cache, fetch from network
+      return fetch(request).then((networkResponse) => {
+        // Only cache successful responses
+        if (!networkResponse || networkResponse.status !== 200 || networkResponse.type === 'error') {
+          return networkResponse;
+        }
+
+        // Cache specific file types
+        const shouldCache =
+          url.pathname.endsWith('.css') ||
+          url.pathname.endsWith('.js') ||
+          url.pathname.endsWith('.svg') ||
+          url.pathname.endsWith('.jpg') ||
+          url.pathname.endsWith('.webp') ||
+          url.pathname.endsWith('.woff') ||
+          url.pathname.endsWith('.woff2') ||
+          url.pathname.endsWith('.ttf') ||
+          url.pathname.endsWith('.otf') ||
+          url.pathname.startsWith('/blog/') ||
+          url.pathname.startsWith('/notes/') ||
+          url.pathname === '/';
+
+        if (shouldCache) {
+          const responseToCache = networkResponse.clone();
+          caches.open(CACHE_NAME).then((cache) => {
+            console.log('[SW] Caching new resource:', url.pathname);
+            cache.put(request, responseToCache);
+          });
+        }
+
+        return networkResponse;
+      }).catch((error) => {
+        console.log('[SW] Fetch failed:', error);
+
+        // Return offline page for HTML requests
+        if (request.headers.get('Accept').includes('text/html')) {
+          return caches.match('/offline.html');
+        }
+      });
+    })
+  );
+});
+
+// Handle messages from the client
+self.addEventListener('message', (event) => {
+  if (event.data && event.data.type === 'SKIP_WAITING') {
+    self.skipWaiting();
+  }
+
+  if (event.data && event.data.type === 'CLEAR_CACHE') {
+    event.waitUntil(
+      caches.keys().then((cacheNames) => {
+        return Promise.all(
+          cacheNames.map((cacheName) => caches.delete(cacheName))
+        );
+      })
+    );
+  }
+});
--- a/mrjunejune/src/tools/hls_player/index.css	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/src/tools/hls_player/index.css	Tue Aug 04 16:49:01 2026 -0700
@@ -23,7 +23,7 @@
 .hls-url-row input {
   width: 100%;
   padding: 0.65rem 0.8rem;
-  color: rgb(var(--gray-dark));
+  color: var(--gray-dark);
   background: var(--white);
   border: 1px solid var(--darkgray);
   border-radius: 6px;
--- a/mrjunejune/src/tools/markdown_to_html/index.css	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/src/tools/markdown_to_html/index.css	Tue Aug 04 16:49:01 2026 -0700
@@ -17,7 +17,7 @@
 }
 
 .header h1 {
-  color: rgb(var(--gray-dark));
+  color: var(--gray-dark);
   margin-bottom: 10px;
 }
 
@@ -49,7 +49,7 @@
 
 .label {
   font-weight: 600;
-  color: rgb(var(--gray-dark));
+  color: var(--gray-dark);
   font-size: 14px;
   text-transform: uppercase;
   letter-spacing: 0.5px;
@@ -75,8 +75,8 @@
   flex: 1;
   width: 100%;
   padding: 15px;
-  background: rgb(var(--gray-light));
-  color: rgb(var(--gray-dark));
+  background: var(--gray-light);
+  color: var(--gray-dark);
   border: 1px solid rgba(var(--gray), 0.3);
   border-radius: 4px;
   font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
@@ -97,7 +97,7 @@
   border: 1px solid rgba(var(--gray), 0.3);
   border-radius: 4px;
   background: var(--white);
-  color: rgb(var(--gray-dark));
+  color: var(--gray-dark);
   overflow-y: auto;
   font-size: 15px;
 }
@@ -105,7 +105,7 @@
 /* Markdown output styles */
 #output h1, #output h2, #output h3, #output h4, #output h5, #output h6 {
   margin: 1em 0 0.5em 0;
-  color: rgb(var(--gray-dark));
+  color: var(--gray-dark);
   line-height: 1.3;
 }
 
@@ -132,7 +132,7 @@
 }
 
 #output code {
-  background: rgb(var(--gray-light));
+  background: var(--gray-light);
   padding: 2px 6px;
   border-radius: 3px;
   font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
@@ -160,7 +160,7 @@
   border-left: 4px solid var(--accent);
   padding: 10px 15px;
   margin: 1em 0;
-  background: rgb(var(--gray-light));
+  background: var(--gray-light);
   color: rgb(var(--gray));
   font-style: italic;
 }
@@ -176,7 +176,7 @@
 
 #output hr {
   border: none;
-  border-top: 1px solid rgb(var(--gray-light));
+  border-top: 1px solid var(--gray-light);
   margin: 1.5em 0;
 }
 
--- a/mrjunejune/test/snapshots/blog.snapshot	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/test/snapshots/blog.snapshot	Tue Aug 04 16:49:01 2026 -0700
@@ -3,7 +3,10 @@
   <head>
     <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
+<meta name="theme-color" content="#f7f3e8">
 <link rel="icon" type="image/svg+xml" href="/public/epi_all_colors.svg">
+<link rel="manifest" href="/public/manifest.json">
+<link rel="apple-touch-icon" href="/public/epi_all_colors.svg">
 
 <link rel="preload" href="/public/fonts/Roboto-Regular.ttf" as="font"  crossorigin>
 <link rel="preload" href="/public/fonts/Roboto-Thin.ttf"as="font" crossorigin>
@@ -16,44 +19,58 @@
 <link rel="preload" href="/public/fonts/more-sugar.thin.otf" as="font" type="font/otf" crossorigin>
 <link rel="preload" href="/public/epi_all_colors.svg" as="image">
 
+<link rel="stylesheet" href="/public/design-system/styles/tokens.css" />
+<link rel="stylesheet" href="/public/design-system/styles/themes.css" />
 <link rel="preload" href="/base.css" as="style" />
 <link rel="stylesheet" href="/base.css" />
+<link rel="stylesheet" href="/public/design-system/styles/components.css" />
+<script
+  type="module"
+  src="/public/design-system/components/index.js"
+></script>
 
+<script src="/public/pwa-register.js" defer></script>
 
   </head>
   <body>
       <style>
   :root {
     --header-background: var(--white);
-    --header-color: rgb(var(--black));
+    --header-color: var(--black);
     --link-hover-accent: var(--awesome);
   }
 
-  /* Fixed icon in top left corner */
-  #themeToggle {
+  #themeControl {
     position: fixed;
     top: 20px;
     left: 20px;
-    background: var(--header-background);
-    display: flex;
-    align-items: center;
-    border-radius: 50%;
-    cursor: pointer;
     z-index: 1000;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
     transition: transform 0.2s ease;
   }
 
-  #themeToggle:hover {
+  #themeControl:hover {
     transform: scale(1.05);
   }
 
+  #themeToggle {
+    flex-direction: column;
+    min-width: 4.5rem;
+    padding: 0.35rem;
+    border-radius: 1.5rem;
+  }
+
+  #themeName {
+    font-family: "More", sans-serif;
+    font-size: 0.65rem;
+    line-height: 1;
+  }
+
   /* Professional header */
   header {
     margin: auto;
     padding: 1.5em 1em;
     font-family: "More", sans-serif;
-    box-shadow: 0 2px 8px rgba(var(--black), 5%);
+    box-shadow: var(--zen-shadow-sm);
     width: 720px;
     max-width: calc(100% - 2em);
     text-align: center;
@@ -77,7 +94,7 @@
 
   /* Mobile responsiveness */
   @media (max-width: 720px) {
-    #themeToggle {
+    #themeControl {
       top: 15px;
       left: 15px;
     }
@@ -92,7 +109,7 @@
   }
 
   @media (max-width: 480px) {
-    #themeToggle {
+    #themeControl {
       top: 10px;
       left: 10px;
     }
@@ -118,20 +135,25 @@
   }
   
   /* 2. MANUAL DARK OVERRIDE */
-  html.dark {
+  html[data-zen-theme="ink"] {
     --logo-invert: invert(1);
     --epi-grayscale: grayscale(1);
   }
   
   /* 3. MANUAL LIGHT OVERRIDE */
-  html.light-mode {
+  html[data-zen-theme="paper"] {
     --logo-invert: invert(0);
     --epi-grayscale: brightness(2.9) grayscale(1);
   }
+
+  html[data-zen-theme="playful"] {
+    --logo-invert: invert(0);
+    --epi-grayscale: saturate(1.25);
+  }
   
   /* 4. SYSTEM PREFERENCE */
   @media (prefers-color-scheme: dark) {
-    :root:not(.light-mode) {
+    :root:not([data-zen-theme]) {
       --logo-invert: invert(1);
     }
   }
@@ -150,16 +172,29 @@
   }
 </style>
 
-<div id="themeToggle">
-  <img id="epiChan" class="epi-logo" aria-label="Toggle dark mode" src="/public/epi_all_colors.svg" height="50" width="50">
-</div>
+<zen-button id="themeControl" variant="quiet">
+  <button
+    id="themeToggle"
+    type="button"
+    aria-label="Change site theme"
+  >
+    <img
+      id="epiChan"
+      class="epi-logo"
+      alt=""
+      src="/public/epi_all_colors.svg"
+      height="50"
+      width="50"
+    >
+    <span id="themeName" aria-live="polite">Auto</span>
+  </button>
+</zen-button>
 
 <header>
   <h1><a href="/">MrJuneJune</a></h1>
 </header>
 <script src="/index.js"></script>
 
-
      <main>
        <h1 class="title" style="margin-bottom: 20px;"> Blogs </h1>
        <ul style="list-style: none;">
--- a/mrjunejune/test/snapshots/resume.snapshot	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/test/snapshots/resume.snapshot	Tue Aug 04 16:49:01 2026 -0700
@@ -3,7 +3,7 @@
   <head>
     <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
-<meta name="theme-color" content="#544e43">
+<meta name="theme-color" content="#f7f3e8">
 <link rel="icon" type="image/svg+xml" href="/public/epi_all_colors.svg">
 <link rel="manifest" href="/public/manifest.json">
 <link rel="apple-touch-icon" href="/public/epi_all_colors.svg">
@@ -19,12 +19,18 @@
 <link rel="preload" href="/public/fonts/more-sugar.thin.otf" as="font" type="font/otf" crossorigin>
 <link rel="preload" href="/public/epi_all_colors.svg" as="image">
 
+<link rel="stylesheet" href="/public/design-system/styles/tokens.css" />
+<link rel="stylesheet" href="/public/design-system/styles/themes.css" />
 <link rel="preload" href="/base.css" as="style" />
 <link rel="stylesheet" href="/base.css" />
+<link rel="stylesheet" href="/public/design-system/styles/components.css" />
+<script
+  type="module"
+  src="/public/design-system/components/index.js"
+></script>
 
 <script src="/public/pwa-register.js" defer></script>
 
-
     <title>Juntae Park Resume</title>
     <link rel="preload" href="resume.css" as="style" />
     <link rel="stylesheet" href="resume.css" />
@@ -33,35 +39,41 @@
     <style>
   :root {
     --header-background: var(--white);
-    --header-color: rgb(var(--black));
+    --header-color: var(--black);
     --link-hover-accent: var(--awesome);
   }
 
-  /* Fixed icon in top left corner */
-  #themeToggle {
+  #themeControl {
     position: fixed;
     top: 20px;
     left: 20px;
-    background: var(--header-background);
-    display: flex;
-    align-items: center;
-    border-radius: 50%;
-    cursor: pointer;
     z-index: 1000;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
     transition: transform 0.2s ease;
   }
 
-  #themeToggle:hover {
+  #themeControl:hover {
     transform: scale(1.05);
   }
 
+  #themeToggle {
+    flex-direction: column;
+    min-width: 4.5rem;
+    padding: 0.35rem;
+    border-radius: 1.5rem;
+  }
+
+  #themeName {
+    font-family: "More", sans-serif;
+    font-size: 0.65rem;
+    line-height: 1;
+  }
+
   /* Professional header */
   header {
     margin: auto;
     padding: 1.5em 1em;
     font-family: "More", sans-serif;
-    box-shadow: 0 2px 8px rgba(var(--black), 5%);
+    box-shadow: var(--zen-shadow-sm);
     width: 720px;
     max-width: calc(100% - 2em);
     text-align: center;
@@ -85,7 +97,7 @@
 
   /* Mobile responsiveness */
   @media (max-width: 720px) {
-    #themeToggle {
+    #themeControl {
       top: 15px;
       left: 15px;
     }
@@ -100,7 +112,7 @@
   }
 
   @media (max-width: 480px) {
-    #themeToggle {
+    #themeControl {
       top: 10px;
       left: 10px;
     }
@@ -126,20 +138,25 @@
   }
   
   /* 2. MANUAL DARK OVERRIDE */
-  html.dark {
+  html[data-zen-theme="ink"] {
     --logo-invert: invert(1);
     --epi-grayscale: grayscale(1);
   }
   
   /* 3. MANUAL LIGHT OVERRIDE */
-  html.light-mode {
+  html[data-zen-theme="paper"] {
     --logo-invert: invert(0);
     --epi-grayscale: brightness(2.9) grayscale(1);
   }
+
+  html[data-zen-theme="playful"] {
+    --logo-invert: invert(0);
+    --epi-grayscale: saturate(1.25);
+  }
   
   /* 4. SYSTEM PREFERENCE */
   @media (prefers-color-scheme: dark) {
-    :root:not(.light-mode) {
+    :root:not([data-zen-theme]) {
       --logo-invert: invert(1);
     }
   }
@@ -158,16 +175,29 @@
   }
 </style>
 
-<div id="themeToggle">
-  <img id="epiChan" class="epi-logo" aria-label="Toggle dark mode" src="/public/epi_all_colors.svg" height="50" width="50">
-</div>
+<zen-button id="themeControl" variant="quiet">
+  <button
+    id="themeToggle"
+    type="button"
+    aria-label="Change site theme"
+  >
+    <img
+      id="epiChan"
+      class="epi-logo"
+      alt=""
+      src="/public/epi_all_colors.svg"
+      height="50"
+      width="50"
+    >
+    <span id="themeName" aria-live="polite">Auto</span>
+  </button>
+</zen-button>
 
 <header>
   <h1><a href="/">MrJuneJune</a></h1>
 </header>
 <script src="/index.js"></script>
 
-
     <main>
       <div class="info">
         <a href="/public/resume.pdf">Download PDF</a>
--- a/mrjunejune/test/snapshots/root.snapshot	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/test/snapshots/root.snapshot	Tue Aug 04 16:49:01 2026 -0700
@@ -4,7 +4,10 @@
     <title> MrJuneJune </title>
     <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
+<meta name="theme-color" content="#f7f3e8">
 <link rel="icon" type="image/svg+xml" href="/public/epi_all_colors.svg">
+<link rel="manifest" href="/public/manifest.json">
+<link rel="apple-touch-icon" href="/public/epi_all_colors.svg">
 
 <link rel="preload" href="/public/fonts/Roboto-Regular.ttf" as="font"  crossorigin>
 <link rel="preload" href="/public/fonts/Roboto-Thin.ttf"as="font" crossorigin>
@@ -17,9 +20,17 @@
 <link rel="preload" href="/public/fonts/more-sugar.thin.otf" as="font" type="font/otf" crossorigin>
 <link rel="preload" href="/public/epi_all_colors.svg" as="image">
 
+<link rel="stylesheet" href="/public/design-system/styles/tokens.css" />
+<link rel="stylesheet" href="/public/design-system/styles/themes.css" />
 <link rel="preload" href="/base.css" as="style" />
 <link rel="stylesheet" href="/base.css" />
+<link rel="stylesheet" href="/public/design-system/styles/components.css" />
+<script
+  type="module"
+  src="/public/design-system/components/index.js"
+></script>
 
+<script src="/public/pwa-register.js" defer></script>
 
     <style>
       .epi-photo {
@@ -47,41 +58,104 @@
           margin-bottom: 0.75em;
         }
       }
+
+      #background {
+        position: fixed;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        pointer-events: none;
+        z-index: 0;
+      }
+
+      #game {
+        position: fixed;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        pointer-events: none;
+        z-index: 1;
+        cursor: default;
+      }
+
+      #game.active {
+        pointer-events: auto;
+        z-index: 100;
+      }
+
+      #gameUI {
+        position: fixed;
+        top: 10px;
+        right: 10px;
+        pointer-events: auto;
+        z-index: 101;
+        cursor: pointer;
+      }
+
+      .site-link-grid {
+        display: grid;
+        grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
+        gap: var(--zen-space-4);
+      }
+
+      .site-link-grid > a {
+        color: inherit;
+        text-decoration: none;
+      }
+
+      .site-link-grid zen-card,
+      .site-link-grid article {
+        height: 100%;
+      }
+
+      .site-link-grid p {
+        margin: 0;
+        color: var(--zen-color-text-muted);
+        font-size: 0.9rem;
+      }
     </style>
   </head>
   <body>
      <style>
   :root {
     --header-background: var(--white);
-    --header-color: rgb(var(--black));
+    --header-color: var(--black);
     --link-hover-accent: var(--awesome);
   }
 
-  /* Fixed icon in top left corner */
-  #themeToggle {
+  #themeControl {
     position: fixed;
     top: 20px;
     left: 20px;
-    background: var(--header-background);
-    display: flex;
-    align-items: center;
-    border-radius: 50%;
-    cursor: pointer;
     z-index: 1000;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
     transition: transform 0.2s ease;
   }
 
-  #themeToggle:hover {
+  #themeControl:hover {
     transform: scale(1.05);
   }
 
+  #themeToggle {
+    flex-direction: column;
+    min-width: 4.5rem;
+    padding: 0.35rem;
+    border-radius: 1.5rem;
+  }
+
+  #themeName {
+    font-family: "More", sans-serif;
+    font-size: 0.65rem;
+    line-height: 1;
+  }
+
   /* Professional header */
   header {
     margin: auto;
     padding: 1.5em 1em;
     font-family: "More", sans-serif;
-    box-shadow: 0 2px 8px rgba(var(--black), 5%);
+    box-shadow: var(--zen-shadow-sm);
     width: 720px;
     max-width: calc(100% - 2em);
     text-align: center;
@@ -105,7 +179,7 @@
 
   /* Mobile responsiveness */
   @media (max-width: 720px) {
-    #themeToggle {
+    #themeControl {
       top: 15px;
       left: 15px;
     }
@@ -120,7 +194,7 @@
   }
 
   @media (max-width: 480px) {
-    #themeToggle {
+    #themeControl {
       top: 10px;
       left: 10px;
     }
@@ -146,20 +220,25 @@
   }
   
   /* 2. MANUAL DARK OVERRIDE */
-  html.dark {
+  html[data-zen-theme="ink"] {
     --logo-invert: invert(1);
     --epi-grayscale: grayscale(1);
   }
   
   /* 3. MANUAL LIGHT OVERRIDE */
-  html.light-mode {
+  html[data-zen-theme="paper"] {
     --logo-invert: invert(0);
     --epi-grayscale: brightness(2.9) grayscale(1);
   }
+
+  html[data-zen-theme="playful"] {
+    --logo-invert: invert(0);
+    --epi-grayscale: saturate(1.25);
+  }
   
   /* 4. SYSTEM PREFERENCE */
   @media (prefers-color-scheme: dark) {
-    :root:not(.light-mode) {
+    :root:not([data-zen-theme]) {
       --logo-invert: invert(1);
     }
   }
@@ -178,16 +257,29 @@
   }
 </style>
 
-<div id="themeToggle">
-  <img id="epiChan" class="epi-logo" aria-label="Toggle dark mode" src="/public/epi_all_colors.svg" height="50" width="50">
-</div>
+<zen-button id="themeControl" variant="quiet">
+  <button
+    id="themeToggle"
+    type="button"
+    aria-label="Change site theme"
+  >
+    <img
+      id="epiChan"
+      class="epi-logo"
+      alt=""
+      src="/public/epi_all_colors.svg"
+      height="50"
+      width="50"
+    >
+    <span id="themeName" aria-live="polite">Auto</span>
+  </button>
+</zen-button>
 
 <header>
   <h1><a href="/">MrJuneJune</a></h1>
 </header>
 <script src="/index.js"></script>
 
-
      <main>
        <p>Hi, I am Juntae&mdash;usually June, and occasionally MrJuneJune because the domain name has committed me to the bit.</p>
 
@@ -200,12 +292,31 @@
        </div>
 
        <h2>Links</h2>
-       <ul>
-         <li><a href="https://zenbu.babocoder.com/file/tip">Repository</a> - Check out my code</li>
-         <li><a href="/blog">Blogs</a> - My thoughts / Experiments </li>
-         <li><a href="/resume">Resume</a> - My professional experiences </li>
-         <li><a href="/tools">Tools</a> - Things I made for myself </li>
-       </ul> 
+       <div class="site-link-grid">
+         <a href="https://zenbu.babocoder.com/file/tip">
+           <zen-card interactive>
+             <article><h3>Repository</h3><p>Browse the code behind these experiments.</p></article>
+           </zen-card>
+         </a>
+         <a href="/blog">
+           <zen-card interactive>
+             <article><h3>Blogs</h3><p>Thoughts, experiments, and technical notes.</p></article>
+           </zen-card>
+         </a>
+         <a href="/resume">
+           <zen-card interactive>
+             <article><h3>Resume</h3><p>Professional experience and selected work.</p></article>
+           </zen-card>
+         </a>
+         <a href="/tools">
+           <zen-card interactive>
+             <article><h3>Tools</h3><p>Small things I built for myself.</p></article>
+           </zen-card>
+         </a>
+       </div>
+       <canvas id="background"></canvas>
+       <canvas id="game"></canvas>
+       <canvas id="gameUI"></canvas>
      </main>
      <div style="display: flex; align-items: center; justify-content: center; margin: 30px 0px;">
   <small>&copy; 2026 June Park</small>
@@ -213,6 +324,7 @@
 
   </body>
   <script>
+    // Epi image
     let arr = Array.from({ length: 18 }, (_, i) => i+1);
     function setRandomImages() {
       const randomIndex = Math.floor(Math.random() * arr.length);
@@ -225,4 +337,5 @@
     }
     setRandomImages();
   </script>
+  <script src="/public/dog-game.js"></script>
 </html>
--- a/mrjunejune/test/snapshots/talk.snapshot	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/test/snapshots/talk.snapshot	Tue Aug 04 16:49:01 2026 -0700
@@ -5,7 +5,10 @@
   <title>Talk!</title>
   <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
+<meta name="theme-color" content="#f7f3e8">
 <link rel="icon" type="image/svg+xml" href="/public/epi_all_colors.svg">
+<link rel="manifest" href="/public/manifest.json">
+<link rel="apple-touch-icon" href="/public/epi_all_colors.svg">
 
 <link rel="preload" href="/public/fonts/Roboto-Regular.ttf" as="font"  crossorigin>
 <link rel="preload" href="/public/fonts/Roboto-Thin.ttf"as="font" crossorigin>
@@ -18,9 +21,17 @@
 <link rel="preload" href="/public/fonts/more-sugar.thin.otf" as="font" type="font/otf" crossorigin>
 <link rel="preload" href="/public/epi_all_colors.svg" as="image">
 
+<link rel="stylesheet" href="/public/design-system/styles/tokens.css" />
+<link rel="stylesheet" href="/public/design-system/styles/themes.css" />
 <link rel="preload" href="/base.css" as="style" />
 <link rel="stylesheet" href="/base.css" />
+<link rel="stylesheet" href="/public/design-system/styles/components.css" />
+<script
+  type="module"
+  src="/public/design-system/components/index.js"
+></script>
 
+<script src="/public/pwa-register.js" defer></script>
 
   <style>
     body { font-family: sans-serif; padding: 20px; }
@@ -33,35 +44,41 @@
   <style>
   :root {
     --header-background: var(--white);
-    --header-color: rgb(var(--black));
+    --header-color: var(--black);
     --link-hover-accent: var(--awesome);
   }
 
-  /* Fixed icon in top left corner */
-  #themeToggle {
+  #themeControl {
     position: fixed;
     top: 20px;
     left: 20px;
-    background: var(--header-background);
-    display: flex;
-    align-items: center;
-    border-radius: 50%;
-    cursor: pointer;
     z-index: 1000;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
     transition: transform 0.2s ease;
   }
 
-  #themeToggle:hover {
+  #themeControl:hover {
     transform: scale(1.05);
   }
 
+  #themeToggle {
+    flex-direction: column;
+    min-width: 4.5rem;
+    padding: 0.35rem;
+    border-radius: 1.5rem;
+  }
+
+  #themeName {
+    font-family: "More", sans-serif;
+    font-size: 0.65rem;
+    line-height: 1;
+  }
+
   /* Professional header */
   header {
     margin: auto;
     padding: 1.5em 1em;
     font-family: "More", sans-serif;
-    box-shadow: 0 2px 8px rgba(var(--black), 5%);
+    box-shadow: var(--zen-shadow-sm);
     width: 720px;
     max-width: calc(100% - 2em);
     text-align: center;
@@ -85,7 +102,7 @@
 
   /* Mobile responsiveness */
   @media (max-width: 720px) {
-    #themeToggle {
+    #themeControl {
       top: 15px;
       left: 15px;
     }
@@ -100,7 +117,7 @@
   }
 
   @media (max-width: 480px) {
-    #themeToggle {
+    #themeControl {
       top: 10px;
       left: 10px;
     }
@@ -126,20 +143,25 @@
   }
   
   /* 2. MANUAL DARK OVERRIDE */
-  html.dark {
+  html[data-zen-theme="ink"] {
     --logo-invert: invert(1);
     --epi-grayscale: grayscale(1);
   }
   
   /* 3. MANUAL LIGHT OVERRIDE */
-  html.light-mode {
+  html[data-zen-theme="paper"] {
     --logo-invert: invert(0);
     --epi-grayscale: brightness(2.9) grayscale(1);
   }
+
+  html[data-zen-theme="playful"] {
+    --logo-invert: invert(0);
+    --epi-grayscale: saturate(1.25);
+  }
   
   /* 4. SYSTEM PREFERENCE */
   @media (prefers-color-scheme: dark) {
-    :root:not(.light-mode) {
+    :root:not([data-zen-theme]) {
       --logo-invert: invert(1);
     }
   }
@@ -158,16 +180,29 @@
   }
 </style>
 
-<div id="themeToggle">
-  <img id="epiChan" class="epi-logo" aria-label="Toggle dark mode" src="/public/epi_all_colors.svg" height="50" width="50">
-</div>
+<zen-button id="themeControl" variant="quiet">
+  <button
+    id="themeToggle"
+    type="button"
+    aria-label="Change site theme"
+  >
+    <img
+      id="epiChan"
+      class="epi-logo"
+      alt=""
+      src="/public/epi_all_colors.svg"
+      height="50"
+      width="50"
+    >
+    <span id="themeName" aria-live="polite">Auto</span>
+  </button>
+</zen-button>
 
 <header>
   <h1><a href="/">MrJuneJune</a></h1>
 </header>
 <script src="/index.js"></script>
 
-
   <main>
     <h1>Talk with strangers xDDD</h1>
 
--- a/mrjunejune/test/snapshots/tools.snapshot	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/test/snapshots/tools.snapshot	Tue Aug 04 16:49:01 2026 -0700
@@ -3,7 +3,7 @@
   <head>
     <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
-<meta name="theme-color" content="#544e43">
+<meta name="theme-color" content="#f7f3e8">
 <link rel="icon" type="image/svg+xml" href="/public/epi_all_colors.svg">
 <link rel="manifest" href="/public/manifest.json">
 <link rel="apple-touch-icon" href="/public/epi_all_colors.svg">
@@ -19,47 +19,59 @@
 <link rel="preload" href="/public/fonts/more-sugar.thin.otf" as="font" type="font/otf" crossorigin>
 <link rel="preload" href="/public/epi_all_colors.svg" as="image">
 
+<link rel="stylesheet" href="/public/design-system/styles/tokens.css" />
+<link rel="stylesheet" href="/public/design-system/styles/themes.css" />
 <link rel="preload" href="/base.css" as="style" />
 <link rel="stylesheet" href="/base.css" />
+<link rel="stylesheet" href="/public/design-system/styles/components.css" />
+<script
+  type="module"
+  src="/public/design-system/components/index.js"
+></script>
 
 <script src="/public/pwa-register.js" defer></script>
 
-
     <link rel="stylesheet" href="/tools/index.css" />
   </head>
   <body>
       <style>
   :root {
     --header-background: var(--white);
-    --header-color: rgb(var(--black));
+    --header-color: var(--black);
     --link-hover-accent: var(--awesome);
   }
 
-  /* Fixed icon in top left corner */
-  #themeToggle {
+  #themeControl {
     position: fixed;
     top: 20px;
     left: 20px;
-    background: var(--header-background);
-    display: flex;
-    align-items: center;
-    border-radius: 50%;
-    cursor: pointer;
     z-index: 1000;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
     transition: transform 0.2s ease;
   }
 
-  #themeToggle:hover {
+  #themeControl:hover {
     transform: scale(1.05);
   }
 
+  #themeToggle {
+    flex-direction: column;
+    min-width: 4.5rem;
+    padding: 0.35rem;
+    border-radius: 1.5rem;
+  }
+
+  #themeName {
+    font-family: "More", sans-serif;
+    font-size: 0.65rem;
+    line-height: 1;
+  }
+
   /* Professional header */
   header {
     margin: auto;
     padding: 1.5em 1em;
     font-family: "More", sans-serif;
-    box-shadow: 0 2px 8px rgba(var(--black), 5%);
+    box-shadow: var(--zen-shadow-sm);
     width: 720px;
     max-width: calc(100% - 2em);
     text-align: center;
@@ -83,7 +95,7 @@
 
   /* Mobile responsiveness */
   @media (max-width: 720px) {
-    #themeToggle {
+    #themeControl {
       top: 15px;
       left: 15px;
     }
@@ -98,7 +110,7 @@
   }
 
   @media (max-width: 480px) {
-    #themeToggle {
+    #themeControl {
       top: 10px;
       left: 10px;
     }
@@ -124,20 +136,25 @@
   }
   
   /* 2. MANUAL DARK OVERRIDE */
-  html.dark {
+  html[data-zen-theme="ink"] {
     --logo-invert: invert(1);
     --epi-grayscale: grayscale(1);
   }
   
   /* 3. MANUAL LIGHT OVERRIDE */
-  html.light-mode {
+  html[data-zen-theme="paper"] {
     --logo-invert: invert(0);
     --epi-grayscale: brightness(2.9) grayscale(1);
   }
+
+  html[data-zen-theme="playful"] {
+    --logo-invert: invert(0);
+    --epi-grayscale: saturate(1.25);
+  }
   
   /* 4. SYSTEM PREFERENCE */
   @media (prefers-color-scheme: dark) {
-    :root:not(.light-mode) {
+    :root:not([data-zen-theme]) {
       --logo-invert: invert(1);
     }
   }
@@ -156,16 +173,29 @@
   }
 </style>
 
-<div id="themeToggle">
-  <img id="epiChan" class="epi-logo" aria-label="Toggle dark mode" src="/public/epi_all_colors.svg" height="50" width="50">
-</div>
+<zen-button id="themeControl" variant="quiet">
+  <button
+    id="themeToggle"
+    type="button"
+    aria-label="Change site theme"
+  >
+    <img
+      id="epiChan"
+      class="epi-logo"
+      alt=""
+      src="/public/epi_all_colors.svg"
+      height="50"
+      width="50"
+    >
+    <span id="themeName" aria-live="polite">Auto</span>
+  </button>
+</zen-button>
 
 <header>
   <h1><a href="/">MrJuneJune</a></h1>
 </header>
 <script src="/index.js"></script>
 
-
      <main>
        <h1 class="title"> Tools </h1>
        <ul class="nav-list">
--- a/mrjunejune/test/snapshots/tools_file_converter.snapshot	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/test/snapshots/tools_file_converter.snapshot	Tue Aug 04 16:49:01 2026 -0700
@@ -6,7 +6,10 @@
     <title>File Format Converter</title>
     <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
+<meta name="theme-color" content="#f7f3e8">
 <link rel="icon" type="image/svg+xml" href="/public/epi_all_colors.svg">
+<link rel="manifest" href="/public/manifest.json">
+<link rel="apple-touch-icon" href="/public/epi_all_colors.svg">
 
 <link rel="preload" href="/public/fonts/Roboto-Regular.ttf" as="font"  crossorigin>
 <link rel="preload" href="/public/fonts/Roboto-Thin.ttf"as="font" crossorigin>
@@ -19,9 +22,17 @@
 <link rel="preload" href="/public/fonts/more-sugar.thin.otf" as="font" type="font/otf" crossorigin>
 <link rel="preload" href="/public/epi_all_colors.svg" as="image">
 
+<link rel="stylesheet" href="/public/design-system/styles/tokens.css" />
+<link rel="stylesheet" href="/public/design-system/styles/themes.css" />
 <link rel="preload" href="/base.css" as="style" />
 <link rel="stylesheet" href="/base.css" />
+<link rel="stylesheet" href="/public/design-system/styles/components.css" />
+<script
+  type="module"
+  src="/public/design-system/components/index.js"
+></script>
 
+<script src="/public/pwa-register.js" defer></script>
 
     <style>
         .container {
@@ -157,35 +168,41 @@
     <style>
   :root {
     --header-background: var(--white);
-    --header-color: rgb(var(--black));
+    --header-color: var(--black);
     --link-hover-accent: var(--awesome);
   }
 
-  /* Fixed icon in top left corner */
-  #themeToggle {
+  #themeControl {
     position: fixed;
     top: 20px;
     left: 20px;
-    background: var(--header-background);
-    display: flex;
-    align-items: center;
-    border-radius: 50%;
-    cursor: pointer;
     z-index: 1000;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
     transition: transform 0.2s ease;
   }
 
-  #themeToggle:hover {
+  #themeControl:hover {
     transform: scale(1.05);
   }
 
+  #themeToggle {
+    flex-direction: column;
+    min-width: 4.5rem;
+    padding: 0.35rem;
+    border-radius: 1.5rem;
+  }
+
+  #themeName {
+    font-family: "More", sans-serif;
+    font-size: 0.65rem;
+    line-height: 1;
+  }
+
   /* Professional header */
   header {
     margin: auto;
     padding: 1.5em 1em;
     font-family: "More", sans-serif;
-    box-shadow: 0 2px 8px rgba(var(--black), 5%);
+    box-shadow: var(--zen-shadow-sm);
     width: 720px;
     max-width: calc(100% - 2em);
     text-align: center;
@@ -209,7 +226,7 @@
 
   /* Mobile responsiveness */
   @media (max-width: 720px) {
-    #themeToggle {
+    #themeControl {
       top: 15px;
       left: 15px;
     }
@@ -224,7 +241,7 @@
   }
 
   @media (max-width: 480px) {
-    #themeToggle {
+    #themeControl {
       top: 10px;
       left: 10px;
     }
@@ -250,20 +267,25 @@
   }
   
   /* 2. MANUAL DARK OVERRIDE */
-  html.dark {
+  html[data-zen-theme="ink"] {
     --logo-invert: invert(1);
     --epi-grayscale: grayscale(1);
   }
   
   /* 3. MANUAL LIGHT OVERRIDE */
-  html.light-mode {
+  html[data-zen-theme="paper"] {
     --logo-invert: invert(0);
     --epi-grayscale: brightness(2.9) grayscale(1);
   }
+
+  html[data-zen-theme="playful"] {
+    --logo-invert: invert(0);
+    --epi-grayscale: saturate(1.25);
+  }
   
   /* 4. SYSTEM PREFERENCE */
   @media (prefers-color-scheme: dark) {
-    :root:not(.light-mode) {
+    :root:not([data-zen-theme]) {
       --logo-invert: invert(1);
     }
   }
@@ -282,9 +304,23 @@
   }
 </style>
 
-<div id="themeToggle">
-  <img id="epiChan" class="epi-logo" aria-label="Toggle dark mode" src="/public/epi_all_colors.svg" height="50" width="50">
-</div>
+<zen-button id="themeControl" variant="quiet">
+  <button
+    id="themeToggle"
+    type="button"
+    aria-label="Change site theme"
+  >
+    <img
+      id="epiChan"
+      class="epi-logo"
+      alt=""
+      src="/public/epi_all_colors.svg"
+      height="50"
+      width="50"
+    >
+    <span id="themeName" aria-live="polite">Auto</span>
+  </button>
+</zen-button>
 
 <header>
   <h1><a href="/">MrJuneJune</a></h1>
@@ -292,7 +328,6 @@
 <script src="/index.js"></script>
 
 
-
     <div class="container">
         <h1>File Format Converter</h1>
         <p>Convert your images and videos to different formats using FFmpeg</p>
--- a/mrjunejune/test/snapshots/tools_file_converter_index.html.snapshot	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/test/snapshots/tools_file_converter_index.html.snapshot	Tue Aug 04 16:49:01 2026 -0700
@@ -1,332 +0,0 @@
- as="font" type="font/woff" crossorigin> -->
-<!-- <link rel="preload" href="/public/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin> -->
-
-<!-- <link rel="preload" href="/public/fonts/more-sugar.extras.otf" as="font" type="font/otf" crossorigin> -->
-<link rel="preload" href="/public/fonts/more-sugar.regular.otf" as="font" type="font/otf" crossorigin>
-<link rel="preload" href="/public/fonts/more-sugar.thin.otf" as="font" type="font/otf" crossorigin>
-<link rel="preload" href="/public/epi_all_colors.svg" as="image">
-
-<link rel="preload" href="/base.css" as="style" />
-<link rel="stylesheet" href="/base.css" />
-
-
-    <style>
-        .container {
-            max-width: 800px;
-            margin: 2rem auto;
-            padding: 2rem;
-        }
-
-        .converter-section {
-            border-radius: 8px;
-            border: 2px solid var(--lightgray);
-            padding: 2rem;
-            margin-bottom: 2rem;
-        }
-
-        .converter-section h2 {
-            margin-top: 0;
-            color: var(--black);
-        }
-
-        .file-input-wrapper {
-            margin: 1rem 0;
-        }
-
-        .file-input-wrapper label {
-            display: block;
-            margin-bottom: 0.5rem;
-            font-weight: 600;
-        }
-
-        input[type="file"] {
-            padding: 0.75rem;
-            border: 2px dashed #ccc;
-            border-radius: 4px;
-            background: white;
-            font-size: 16px;
-        }
-
-        button {
-            background: var(--purple);
-            font-family: "More Thin", sans-serif;
-            color: var(--gray-gradient);
-            border: none;
-            padding: 0.75rem 1.5rem;
-            border-radius: 4px;
-            cursor: pointer;
-            font-size: 1rem;
-            margin-top: 1rem;
-            min-height: 44px;
-        }
-
-        button:hover {
-            background: var(--orange);
-        }
-
-        button:disabled {
-            background: var(--gray);
-            cursor: not-allowed;
-        }
-
-        .result {
-            margin-top: 1rem;
-            padding: 1rem;
-            background: white;
-            border-radius: 4px;
-            display: none;
-        }
-
-        .result.show {
-            display: block;
-        }
-
-        .result.success {
-            border-left: 4px solid var(--blue);
-        }
-
-        .result.error {
-            border-left: 4px solid var(--red);
-        }
-
-        .download-link {
-            display: inline-block;
-            margin-top: 0.5rem;
-            color: var(--awesome);
-            text-decoration: none;
-        }
-
-        .download-link:hover {
-            text-decoration: underline;
-        }
-
-        .loading {
-            display: none;
-            margin-top: 1rem;
-        }
-
-        .loading.show {
-            display: block;
-        }
-
-        /* Mobile responsive */
-        @media (max-width: 720px) {
-            .container {
-                padding: 1rem;
-                margin: 1rem auto;
-            }
-
-            .converter-section {
-                padding: 1.5rem 1rem;
-            }
-
-            .converter-section h2 {
-                font-size: 1.5rem;
-            }
-
-            .file-input-wrapper label {
-                font-size: 1rem;
-            }
-
-            button {
-                font-size: 1.1rem;
-                padding: 1rem 1.5rem;
-            }
-
-            .result {
-                padding: 1rem;
-                font-size: 1rem;
-            }
-        }
-    </style>
-</head>
-<body>
-    <style>
-  :root {
-    --header-background: var(--white);
-    --header-color: rgb(var(--black));
-    --link-hover-accent: var(--awesome);
-  }
-
-  /* Fixed icon in top left corner */
-  #themeToggle {
-    position: fixed;
-    top: 20px;
-    left: 20px;
-    background: var(--header-background);
-    display: flex;
-    align-items: center;
-    border-radius: 50%;
-    cursor: pointer;
-    z-index: 1000;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-    transition: transform 0.2s ease;
-  }
-
-  #themeToggle:hover {
-    transform: scale(1.05);
-  }
-
-  /* Professional header */
-  header {
-    margin: auto;
-    padding: 1.5em 1em;
-    font-family: "More", sans-serif;
-    box-shadow: 0 2px 8px rgba(var(--black), 5%);
-    width: 720px;
-    max-width: calc(100% - 2em);
-    text-align: center;
-  }
-
-  header h1 {
-    margin: 0;
-    font-size: 1.8em;
-    font-weight: 700;
-    letter-spacing: -0.5px;
-  }
-
-  header h1 a {
-    text-decoration: none;
-    color: var(--header-color);
-  }
-
-  header h1 a::before {
-    display: none;
-  }
-
-  /* Mobile responsiveness */
-  @media (max-width: 720px) {
-    #themeToggle {
-      top: 15px;
-      left: 15px;
-    }
-
-    header {
-      padding: 1em;
-    }
-
-    header h1 {
-      font-size: 1.5em;
-    }
-  }
-
-  @media (max-width: 480px) {
-    #themeToggle {
-      top: 10px;
-      left: 10px;
-    }
-
-    #themeToggle img {
-      height: 40px;
-      width: 40px;
-    }
-
-    header h1 {
-      font-size: 1.3em;
-    }
-  }
-
-  #logo {
-    width: 300px;
-  }
-
-  /* 1. DEFINE THE DEFAULTS (Light Mode) */
-  :root {
-    --logo-invert: invert(0);
-    --epi-grayscale: grayscale(0) brightness(1);
-  }
-  
-  /* 2. MANUAL DARK OVERRIDE */
-  html.dark {
-    --logo-invert: invert(1);
-    --epi-grayscale: grayscale(1);
-  }
-  
-  /* 3. MANUAL LIGHT OVERRIDE */
-  html.light-mode {
-    --logo-invert: invert(0);
-    --epi-grayscale: brightness(2.9) grayscale(1);
-  }
-  
-  /* 4. SYSTEM PREFERENCE */
-  @media (prefers-color-scheme: dark) {
-    :root:not(.light-mode) {
-      --logo-invert: invert(1);
-    }
-  }
-  
-  /* 5. APPLY TO ELEMENTS */
-  #logo {
-    -webkit-filter: var(--logo-invert);
-    filter: var(--logo-invert);
-    transition: filter 0.3s ease;
-  }
-  
-  .epi-logo {
-    -webkit-filter: var(--epi-grayscale);
-    filter: var(--epi-grayscale);
-    transition: filter 0.3s ease;
-  }
-</style>
-
-<div id="themeToggle">
-  <img id="epiChan" class="epi-logo" aria-label="Toggle dark mode" src="/public/epi_all_colors.svg" height="50" width="50">
-</div>
-
-<header>
-  <h1><a href="/">MrJuneJune</a></h1>
-</header>
-<script src="/index.js"></script>
-
-
-
-    <div class="container">
-        <h1>File Format Converter</h1>
-        <p>Convert your images and videos to different formats using FFmpeg</p>
-
-        <div class="converter-section">
-            <h2>Image to WebP Converter</h2>
-            <p>Upload an image file (PNG, JPG, GIF, etc.) to convert it to WebP format</p>
-
-            <div class="file-input-wrapper">
-                <label for="imageInput">Choose an image file:</label>
-                <input type="file" id="imageInput" accept="image/*">
-            </div>
-
-            <button id="convertImageBtn" onclick="convertImageToWebP()">Convert to WebP</button>
-
-            <div class="loading" id="imageLoading">Converting... Please wait.</div>
-
-            <div class="result" id="imageResult">
-                <p id="imageMessage"></p>
-                <a id="imageDownload" class="download-link" style="display: none;">Download WebP Image</a>
-            </div>
-        </div>
-
-        <div class="converter-section">
-            <h2>Video to MP4 Converter</h2>
-            <p>Upload a video file (AVI, MOV, MKV, etc.) to convert it to MP4 format</p>
-
-            <div class="file-input-wrapper">
-                <label for="videoInput">Choose a video file:</label>
-                <input type="file" id="videoInput" accept="video/*">
-            </div>
-
-            <button id="convertVideoBtn" onclick="convertVideoToMP4()">Convert to MP4</button>
-
-            <div class="loading" id="videoLoading">Converting... Please wait.</div>
-
-            <div class="result" id="videoResult">
-                <p id="videoMessage"></p>
-                <a id="videoDownload" class="download-link" style="display: none;">Download MP4 Video</a>
-            </div>
-        </div>
-        <div style="display: flex; align-items: center; justify-content: center; margin: 30px 0px;">
-  <small>&copy; 2026 June Park</small>
-</div>
-
-    </div>
-    <script src="/tools/file_converter/index.js"></script>
-</body>
-</htmlLocation: /tools/file_converter
-
--- a/mrjunejune/test/snapshots/tools_hls_player.snapshot	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/test/snapshots/tools_hls_player.snapshot	Tue Aug 04 16:49:01 2026 -0700
@@ -3,7 +3,7 @@
   <head>
     <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
-<meta name="theme-color" content="#544e43">
+<meta name="theme-color" content="#f7f3e8">
 <link rel="icon" type="image/svg+xml" href="/public/epi_all_colors.svg">
 <link rel="manifest" href="/public/manifest.json">
 <link rel="apple-touch-icon" href="/public/epi_all_colors.svg">
@@ -19,12 +19,18 @@
 <link rel="preload" href="/public/fonts/more-sugar.thin.otf" as="font" type="font/otf" crossorigin>
 <link rel="preload" href="/public/epi_all_colors.svg" as="image">
 
+<link rel="stylesheet" href="/public/design-system/styles/tokens.css" />
+<link rel="stylesheet" href="/public/design-system/styles/themes.css" />
 <link rel="preload" href="/base.css" as="style" />
 <link rel="stylesheet" href="/base.css" />
+<link rel="stylesheet" href="/public/design-system/styles/components.css" />
+<script
+  type="module"
+  src="/public/design-system/components/index.js"
+></script>
 
 <script src="/public/pwa-register.js" defer></script>
 
-
     <title>HLS Player - MrJuneJune</title>
     <link rel="stylesheet" href="/tools/hls_player/index.css" />
     <script src="/public/hls.min.js" defer></script>
@@ -34,35 +40,41 @@
     <style>
   :root {
     --header-background: var(--white);
-    --header-color: rgb(var(--black));
+    --header-color: var(--black);
     --link-hover-accent: var(--awesome);
   }
 
-  /* Fixed icon in top left corner */
-  #themeToggle {
+  #themeControl {
     position: fixed;
     top: 20px;
     left: 20px;
-    background: var(--header-background);
-    display: flex;
-    align-items: center;
-    border-radius: 50%;
-    cursor: pointer;
     z-index: 1000;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
     transition: transform 0.2s ease;
   }
 
-  #themeToggle:hover {
+  #themeControl:hover {
     transform: scale(1.05);
   }
 
+  #themeToggle {
+    flex-direction: column;
+    min-width: 4.5rem;
+    padding: 0.35rem;
+    border-radius: 1.5rem;
+  }
+
+  #themeName {
+    font-family: "More", sans-serif;
+    font-size: 0.65rem;
+    line-height: 1;
+  }
+
   /* Professional header */
   header {
     margin: auto;
     padding: 1.5em 1em;
     font-family: "More", sans-serif;
-    box-shadow: 0 2px 8px rgba(var(--black), 5%);
+    box-shadow: var(--zen-shadow-sm);
     width: 720px;
     max-width: calc(100% - 2em);
     text-align: center;
@@ -86,7 +98,7 @@
 
   /* Mobile responsiveness */
   @media (max-width: 720px) {
-    #themeToggle {
+    #themeControl {
       top: 15px;
       left: 15px;
     }
@@ -101,7 +113,7 @@
   }
 
   @media (max-width: 480px) {
-    #themeToggle {
+    #themeControl {
       top: 10px;
       left: 10px;
     }
@@ -127,20 +139,25 @@
   }
   
   /* 2. MANUAL DARK OVERRIDE */
-  html.dark {
+  html[data-zen-theme="ink"] {
     --logo-invert: invert(1);
     --epi-grayscale: grayscale(1);
   }
   
   /* 3. MANUAL LIGHT OVERRIDE */
-  html.light-mode {
+  html[data-zen-theme="paper"] {
     --logo-invert: invert(0);
     --epi-grayscale: brightness(2.9) grayscale(1);
   }
+
+  html[data-zen-theme="playful"] {
+    --logo-invert: invert(0);
+    --epi-grayscale: saturate(1.25);
+  }
   
   /* 4. SYSTEM PREFERENCE */
   @media (prefers-color-scheme: dark) {
-    :root:not(.light-mode) {
+    :root:not([data-zen-theme]) {
       --logo-invert: invert(1);
     }
   }
@@ -159,16 +176,29 @@
   }
 </style>
 
-<div id="themeToggle">
-  <img id="epiChan" class="epi-logo" aria-label="Toggle dark mode" src="/public/epi_all_colors.svg" height="50" width="50">
-</div>
+<zen-button id="themeControl" variant="quiet">
+  <button
+    id="themeToggle"
+    type="button"
+    aria-label="Change site theme"
+  >
+    <img
+      id="epiChan"
+      class="epi-logo"
+      alt=""
+      src="/public/epi_all_colors.svg"
+      height="50"
+      width="50"
+    >
+    <span id="themeName" aria-live="polite">Auto</span>
+  </button>
+</zen-button>
 
 <header>
   <h1><a href="/">MrJuneJune</a></h1>
 </header>
 <script src="/index.js"></script>
 
-
     <main>
       <h1>Online HLS Player</h1>
       <p>Load an HLS playlist from a URL, or try the sample stream bundled with this site.</p>
--- a/mrjunejune/test/snapshots/tools_latex_editor.snapshot	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/test/snapshots/tools_latex_editor.snapshot	Tue Aug 04 16:49:01 2026 -0700
@@ -3,7 +3,7 @@
   <head>
     <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
-<meta name="theme-color" content="#544e43">
+<meta name="theme-color" content="#f7f3e8">
 <link rel="icon" type="image/svg+xml" href="/public/epi_all_colors.svg">
 <link rel="manifest" href="/public/manifest.json">
 <link rel="apple-touch-icon" href="/public/epi_all_colors.svg">
@@ -19,12 +19,18 @@
 <link rel="preload" href="/public/fonts/more-sugar.thin.otf" as="font" type="font/otf" crossorigin>
 <link rel="preload" href="/public/epi_all_colors.svg" as="image">
 
+<link rel="stylesheet" href="/public/design-system/styles/tokens.css" />
+<link rel="stylesheet" href="/public/design-system/styles/themes.css" />
 <link rel="preload" href="/base.css" as="style" />
 <link rel="stylesheet" href="/base.css" />
+<link rel="stylesheet" href="/public/design-system/styles/components.css" />
+<script
+  type="module"
+  src="/public/design-system/components/index.js"
+></script>
 
 <script src="/public/pwa-register.js" defer></script>
 
-
     <title>LaTeX Editor - MrJuneJune</title>
     <link rel="stylesheet" href="/tools/latex_editor/index.css" />
     <script src="/tools/latex_editor/index.js" defer></script>
@@ -33,35 +39,41 @@
     <style>
   :root {
     --header-background: var(--white);
-    --header-color: rgb(var(--black));
+    --header-color: var(--black);
     --link-hover-accent: var(--awesome);
   }
 
-  /* Fixed icon in top left corner */
-  #themeToggle {
+  #themeControl {
     position: fixed;
     top: 20px;
     left: 20px;
-    background: var(--header-background);
-    display: flex;
-    align-items: center;
-    border-radius: 50%;
-    cursor: pointer;
     z-index: 1000;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
     transition: transform 0.2s ease;
   }
 
-  #themeToggle:hover {
+  #themeControl:hover {
     transform: scale(1.05);
   }
 
+  #themeToggle {
+    flex-direction: column;
+    min-width: 4.5rem;
+    padding: 0.35rem;
+    border-radius: 1.5rem;
+  }
+
+  #themeName {
+    font-family: "More", sans-serif;
+    font-size: 0.65rem;
+    line-height: 1;
+  }
+
   /* Professional header */
   header {
     margin: auto;
     padding: 1.5em 1em;
     font-family: "More", sans-serif;
-    box-shadow: 0 2px 8px rgba(var(--black), 5%);
+    box-shadow: var(--zen-shadow-sm);
     width: 720px;
     max-width: calc(100% - 2em);
     text-align: center;
@@ -85,7 +97,7 @@
 
   /* Mobile responsiveness */
   @media (max-width: 720px) {
-    #themeToggle {
+    #themeControl {
       top: 15px;
       left: 15px;
     }
@@ -100,7 +112,7 @@
   }
 
   @media (max-width: 480px) {
-    #themeToggle {
+    #themeControl {
       top: 10px;
       left: 10px;
     }
@@ -126,20 +138,25 @@
   }
   
   /* 2. MANUAL DARK OVERRIDE */
-  html.dark {
+  html[data-zen-theme="ink"] {
     --logo-invert: invert(1);
     --epi-grayscale: grayscale(1);
   }
   
   /* 3. MANUAL LIGHT OVERRIDE */
-  html.light-mode {
+  html[data-zen-theme="paper"] {
     --logo-invert: invert(0);
     --epi-grayscale: brightness(2.9) grayscale(1);
   }
+
+  html[data-zen-theme="playful"] {
+    --logo-invert: invert(0);
+    --epi-grayscale: saturate(1.25);
+  }
   
   /* 4. SYSTEM PREFERENCE */
   @media (prefers-color-scheme: dark) {
-    :root:not(.light-mode) {
+    :root:not([data-zen-theme]) {
       --logo-invert: invert(1);
     }
   }
@@ -158,16 +175,29 @@
   }
 </style>
 
-<div id="themeToggle">
-  <img id="epiChan" class="epi-logo" aria-label="Toggle dark mode" src="/public/epi_all_colors.svg" height="50" width="50">
-</div>
+<zen-button id="themeControl" variant="quiet">
+  <button
+    id="themeToggle"
+    type="button"
+    aria-label="Change site theme"
+  >
+    <img
+      id="epiChan"
+      class="epi-logo"
+      alt=""
+      src="/public/epi_all_colors.svg"
+      height="50"
+      width="50"
+    >
+    <span id="themeName" aria-live="polite">Auto</span>
+  </button>
+</zen-button>
 
 <header>
   <h1><a href="/">MrJuneJune</a></h1>
 </header>
 <script src="/index.js"></script>
 
-
     <main class="latex-page">
       <div class="latex-heading">
         <div>
--- a/mrjunejune/test/snapshots/tools_markdown_to_html.snapshot	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/test/snapshots/tools_markdown_to_html.snapshot	Tue Aug 04 16:49:01 2026 -0700
@@ -6,7 +6,10 @@
     <title>Markdown to HTML Converter</title>
     <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
+<meta name="theme-color" content="#f7f3e8">
 <link rel="icon" type="image/svg+xml" href="/public/epi_all_colors.svg">
+<link rel="manifest" href="/public/manifest.json">
+<link rel="apple-touch-icon" href="/public/epi_all_colors.svg">
 
 <link rel="preload" href="/public/fonts/Roboto-Regular.ttf" as="font"  crossorigin>
 <link rel="preload" href="/public/fonts/Roboto-Thin.ttf"as="font" crossorigin>
@@ -19,9 +22,17 @@
 <link rel="preload" href="/public/fonts/more-sugar.thin.otf" as="font" type="font/otf" crossorigin>
 <link rel="preload" href="/public/epi_all_colors.svg" as="image">
 
+<link rel="stylesheet" href="/public/design-system/styles/tokens.css" />
+<link rel="stylesheet" href="/public/design-system/styles/themes.css" />
 <link rel="preload" href="/base.css" as="style" />
 <link rel="stylesheet" href="/base.css" />
+<link rel="stylesheet" href="/public/design-system/styles/components.css" />
+<script
+  type="module"
+  src="/public/design-system/components/index.js"
+></script>
 
+<script src="/public/pwa-register.js" defer></script>
 
     <link rel="stylesheet" href="markdown_to_html/index.css" />
 </head>
@@ -29,35 +40,41 @@
     <style>
   :root {
     --header-background: var(--white);
-    --header-color: rgb(var(--black));
+    --header-color: var(--black);
     --link-hover-accent: var(--awesome);
   }
 
-  /* Fixed icon in top left corner */
-  #themeToggle {
+  #themeControl {
     position: fixed;
     top: 20px;
     left: 20px;
-    background: var(--header-background);
-    display: flex;
-    align-items: center;
-    border-radius: 50%;
-    cursor: pointer;
     z-index: 1000;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
     transition: transform 0.2s ease;
   }
 
-  #themeToggle:hover {
+  #themeControl:hover {
     transform: scale(1.05);
   }
 
+  #themeToggle {
+    flex-direction: column;
+    min-width: 4.5rem;
+    padding: 0.35rem;
+    border-radius: 1.5rem;
+  }
+
+  #themeName {
+    font-family: "More", sans-serif;
+    font-size: 0.65rem;
+    line-height: 1;
+  }
+
   /* Professional header */
   header {
     margin: auto;
     padding: 1.5em 1em;
     font-family: "More", sans-serif;
-    box-shadow: 0 2px 8px rgba(var(--black), 5%);
+    box-shadow: var(--zen-shadow-sm);
     width: 720px;
     max-width: calc(100% - 2em);
     text-align: center;
@@ -81,7 +98,7 @@
 
   /* Mobile responsiveness */
   @media (max-width: 720px) {
-    #themeToggle {
+    #themeControl {
       top: 15px;
       left: 15px;
     }
@@ -96,7 +113,7 @@
   }
 
   @media (max-width: 480px) {
-    #themeToggle {
+    #themeControl {
       top: 10px;
       left: 10px;
     }
@@ -122,20 +139,25 @@
   }
   
   /* 2. MANUAL DARK OVERRIDE */
-  html.dark {
+  html[data-zen-theme="ink"] {
     --logo-invert: invert(1);
     --epi-grayscale: grayscale(1);
   }
   
   /* 3. MANUAL LIGHT OVERRIDE */
-  html.light-mode {
+  html[data-zen-theme="paper"] {
     --logo-invert: invert(0);
     --epi-grayscale: brightness(2.9) grayscale(1);
   }
+
+  html[data-zen-theme="playful"] {
+    --logo-invert: invert(0);
+    --epi-grayscale: saturate(1.25);
+  }
   
   /* 4. SYSTEM PREFERENCE */
   @media (prefers-color-scheme: dark) {
-    :root:not(.light-mode) {
+    :root:not([data-zen-theme]) {
       --logo-invert: invert(1);
     }
   }
@@ -154,16 +176,29 @@
   }
 </style>
 
-<div id="themeToggle">
-  <img id="epiChan" class="epi-logo" aria-label="Toggle dark mode" src="/public/epi_all_colors.svg" height="50" width="50">
-</div>
+<zen-button id="themeControl" variant="quiet">
+  <button
+    id="themeToggle"
+    type="button"
+    aria-label="Change site theme"
+  >
+    <img
+      id="epiChan"
+      class="epi-logo"
+      alt=""
+      src="/public/epi_all_colors.svg"
+      height="50"
+      width="50"
+    >
+    <span id="themeName" aria-live="polite">Auto</span>
+  </button>
+</zen-button>
 
 <header>
   <h1><a href="/">MrJuneJune</a></h1>
 </header>
 <script src="/index.js"></script>
 
-
     <div class="header">
         <h1>Markdown to HTML Converter</h1>
     </div>
@@ -209,7 +244,7 @@
 
 ### Images
 
-![Alt text](https://via.placeholder.com/150)
+![Alt text](https://mrjunejune.com/public/epi_favicon.svg)
 
 **Try editing this text!**</textarea>
         </div>
@@ -226,10 +261,16 @@
   <small>&copy; 2026 June Park</small>
 </div>
 
-    <script src="/markdown_to_html_bin.js"></script>
-    <script>
-        // Wait for WASM module to be ready
-        Module.onRuntimeInitialized = () => {
+    <script type="module">
+        import createModule from '/markdown_to_html_bin.js';
+
+        // Get DOM elements
+        const input = document.getElementById('input');
+        const output = document.getElementById('output');
+        const copy = document.getElementById('copy');
+
+        // Modern Emscripten exports a factory function that returns a Promise
+        createModule().then(Module => {
           // Get raw pointer so we can free it after use
           const markdownToHtmlPtr = Module.cwrap('markdown_to_html', 'number', ['string']);
           const markdownFree = Module.cwrap('markdown_free', null, ['number']);
@@ -266,7 +307,7 @@
               console.error('Failed to copy: ', err);
             });
           });
-        };
+        });
     </script>
 </body>
 </html>
--- a/mrjunejune/test/snapshots/tools_markdown_to_html_index.html.snapshot	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/test/snapshots/tools_markdown_to_html_index.html.snapshot	Tue Aug 04 16:49:01 2026 -0700
@@ -1,388 +0,0 @@
-blic/fonts/more-sugar.extras.otf" as="font" type="font/otf" crossorigin> -->
-<link rel="preload" href="/public/fonts/more-sugar.regular.otf" as="font" type="font/otf" crossorigin>
-<link rel="preload" href="/public/fonts/more-sugar.thin.otf" as="font" type="font/otf" crossorigin>
-<link rel="preload" href="/public/epi_all_colors.svg" as="image">
-
-<link rel="preload" href="/base.css" as="style" />
-<link rel="stylesheet" href="/base.css" />
-
-
-    <link rel="preload" href="resume.css" as="style" />
-    <link rel="stylesheet" href="resume.css" />
-  </head>
-  <body>
-    <style>
-  :root {
-    --header-background: var(--white);
-    --header-color: rgb(var(--black));
-    --link-hover-accent: var(--awesome);
-  }
-
-  /* Fixed icon in top left corner */
-  #themeToggle {
-    position: fixed;
-    top: 20px;
-    left: 20px;
-    background: var(--header-background);
-    display: flex;
-    align-items: center;
-    border-radius: 50%;
-    cursor: pointer;
-    z-index: 1000;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-    transition: transform 0.2s ease;
-  }
-
-  #themeToggle:hover {
-    transform: scale(1.05);
-  }
-
-  /* Professional header */
-  header {
-    margin: auto;
-    padding: 1.5em 1em;
-    font-family: "More", sans-serif;
-    box-shadow: 0 2px 8px rgba(var(--black), 5%);
-    width: 720px;
-    max-width: calc(100% - 2em);
-    text-align: center;
-  }
-
-  header h1 {
-    margin: 0;
-    font-size: 1.8em;
-    font-weight: 700;
-    letter-spacing: -0.5px;
-  }
-
-  header h1 a {
-    text-decoration: none;
-    color: var(--header-color);
-  }
-
-  header h1 a::before {
-    display: none;
-  }
-
-  /* Mobile responsiveness */
-  @media (max-width: 720px) {
-    #themeToggle {
-      top: 15px;
-      left: 15px;
-    }
-
-    header {
-      padding: 1em;
-    }
-
-    header h1 {
-      font-size: 1.5em;
-    }
-  }
-
-  @media (max-width: 480px) {
-    #themeToggle {
-      top: 10px;
-      left: 10px;
-    }
-
-    #themeToggle img {
-      height: 40px;
-      width: 40px;
-    }
-
-    header h1 {
-      font-size: 1.3em;
-    }
-  }
-
-  #logo {
-    width: 300px;
-  }
-
-  /* 1. DEFINE THE DEFAULTS (Light Mode) */
-  :root {
-    --logo-invert: invert(0);
-    --epi-grayscale: grayscale(0) brightness(1);
-  }
-  
-  /* 2. MANUAL DARK OVERRIDE */
-  html.dark {
-    --logo-invert: invert(1);
-    --epi-grayscale: grayscale(1);
-  }
-  
-  /* 3. MANUAL LIGHT OVERRIDE */
-  html.light-mode {
-    --logo-invert: invert(0);
-    --epi-grayscale: brightness(2.9) grayscale(1);
-  }
-  
-  /* 4. SYSTEM PREFERENCE */
-  @media (prefers-color-scheme: dark) {
-    :root:not(.light-mode) {
-      --logo-invert: invert(1);
-    }
-  }
-  
-  /* 5. APPLY TO ELEMENTS */
-  #logo {
-    -webkit-filter: var(--logo-invert);
-    filter: var(--logo-invert);
-    transition: filter 0.3s ease;
-  }
-  
-  .epi-logo {
-    -webkit-filter: var(--epi-grayscale);
-    filter: var(--epi-grayscale);
-    transition: filter 0.3s ease;
-  }
-</style>
-
-<div id="themeToggle">
-  <img id="epiChan" class="epi-logo" aria-label="Toggle dark mode" src="/public/epi_all_colors.svg" height="50" width="50">
-</div>
-
-<header>
-  <h1><a href="/">MrJuneJune</a></h1>
-</header>
-<script src="/index.js"></script>
-
-
-    <main>
-      <div class="info">
-        <a href="/public/resume.pdf">Download PDF</a>
-        <hr>
-        <p><span class="header-firstname-style"> JUNTAE </span><span class="header-lastname-style">PARK</span><p>
-        <p class="header-position-style"> SOFTWARE ENGINEER </p>
-        <div class="header-address-style"> 
-          Bay Area, CA, USA
-        </div>
-        <div class="header-social-style">
-          <div>
-            <a href="tel:+016505311728">(US) 650-531-1728</a>
-          </div>
-          <div>
-            <a href="tel:+014375808026">(CA) 437-580-8026</a>
-          </div>
-          <div>
-            <a href="mailto:[email protected]">[email protected]</a>
-          </div>
-          <div>
-            <a href="https://www.linkedin.com/in/junepark"><svg width="12" height="12" fill="currentColor" class="bi bi-linkedin" viewBox="0 0 16 16">
-               <path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z"/>
-             </svg> June Park
-            </a>
-          </div>
-        </div>
-      </div>
-
-      <div class="sub-header">
-        <h2 class="section-style"> Summary </h2>
-        <div class="line"></div>
-      </div>
-      <p class="paragraph-style">Software Engineer with 9 years of hands-on experience across diverse tech stacks, from early-stage startups to FANG-scale systems. Adept in designing and delivering robust software solutions using modern languages, frameworks, and cloud platforms. <br><br> Open to impactful work.</p>
-      <div class="sub-header">
-
-        <h2 class="section-style"> Skills </h2>
-        <div class="line"></div>
-      </div>
-      <div class="paragraph-style">
-        <p>
-          <span class="entry-title-style"> Programming Languages:</span>
-          <span class="skill-type-style"> TypeScript, Python, C/C++, Ruby, Java, MATLAB </span>
-        </p>
-        <p> 
-          <span class="entry-title-style">Tools & Platforms:</span>
-          <span class="skill-type-style">Bazel, PostgresSQL, Mercurial, Git, Pands, Raylib, XCode</span>
-        </p>
-        <p>
-          <span class="entry-title-style"> Web Frameworks: </span>
-          <span class="skill-type-style"> Django, Rails, React, Flask</span>
-        </p>
-        <p>
-          <span class="entry-title-style"> DevOp:</span>
-          <span class="skill-type-style"> Plummi, Heroku, DigitalOcean, AWS, Google Cloud </span>
-        </p>
-        <p>
-          <span class="entry-title-style"> Language:</span>
-          <span class="skill-type-style"> English, Korean, Japanese </span>
-        </p>
-      </div>
-
-      <!-- Experiences -->
-     <div class="sub-header">
-        <h2 class="section-style"> Experience </h2>
-        <div class="line"></div>
-      </div>
-      <div class="flex-box">
-        <p class="entry-title-style">
-          <a href="https://www.meta.com/">Meta</a>
-        </p>
-        <p class="entry-location-style">San Francisco, CA, USA</p>
-      </div>
-      <div class="flex-box">
-        <p class="entry-position-style">SOFTWARE ENGINEER</p>
-        <p class="entry-date-style">Oct, 2024 - Present</p>
-      </div>
-      <ul class="description-style">
-        <li>
-          Took initiative on Channel Value Rule, targeting the 16% of ad traffic with both app and web destinations to improve value attribution and ROI.
-        </li>
-        <li>
-          Built full-stack features using React and Hack/GraphQL, contributing to scalable, production-ready systems.
-        </li>
-        <li>
-          Partnered with data science to design A/B tests and analyze revenue impact of ads destination.
-        </li>
-        <li>
-          Proposed and implemented alpha improvements to internal testing infrastructure, reducing test time by 50% and enhancing developer velocity.
-        </li>
-      </ul>
-      <div class="flex-box">
-        <p class="entry-title-style">
-          <a href="https://www.wmg.com/">Warner Music Group</a>
-        </p>
-        <p class="entry-location-style">Toronto, ON, Canada</p>
-      </div>
-      <div class="flex-box">
-        <p class="entry-position-style">TECHNICAL LEAD ENGINEER</p>
-        <p class="entry-date-style">July, 2023 - Sept, 2024</p>
-      </div>
-      <ul class="description-style">
-        <li>
-          Implements <a href="https://bazel.build/">bazel </a>structure for the company for TypeScript and JavaScript code base for hermiticity and stablishing standards for JavaScript and
-        </li>
-        <li>
-          TypeScript testing and code structures.
-        </li>
-        <li>
-          Led a team of five engineers in building GraphQL endpoints for client-facing applications using Apollo and AppSync, supporting over 2000 RPS and auto scaling depending on request values.
-        </li>
-        <li>
-          Improved application response times by up to 85% for graphQL response by updating database schema and SQL queries, eliminating N+1 queries and lack of indexes.
-        </li>
-        <li>
-          Developed CI/CD pipelines for backend structures.
-        </li>
-        <li>
-          Designed infrastructure for pub/sub, caching, and media processing logic.
-        </li>
-      </ul>
-
-      <div class="flex-box">
-        <p class="entry-title-style">
-          <a href="https://www.google.com/">Google</a>
-        </p>
-        <p class="entry-location-style">Toronto, ON, Canada</p>
-      </div>
-      <div class="flex-box">
-        <p class="entry-position-style">SOFTWARE ENGINEER</p>
-        <p class="entry-date-style">Feb, 2022 - July 2023</p>
-      </div>
-      <ul class="description-style">
-        <li>
-          Implements and maintained new features relating to App Script across google workspace platform including Gmail, sheets, and Docs.</li>
-        <li>
-          Improved a response time and render time of App Script hover card components.</li>
-        <li>
-          Collaborated with a team of developers to ensure timely and accurate delivery of features.</li>
-        <li>
-          Conducted user testing and gathered feedback to iterate on features for optimal user experience.</li>
-      </ul>
-
-      <div class="flex-box">
-        <p class="entry-title-style">
-          <a href="https://www.everlywell.com/">Everlywell</a>
-        </p>
-        <p class="entry-location-style">Toronto, ON, Canada</p>
-      </div>
-      <div class="flex-box">
-        <p class="entry-position-style">SOFTWARE ENGINEER</p>
-        <p class="entry-date-style">December, 2020 - Jan, 2022</p>
-      </div>
-      <ul class="description-style">
-        <li>
-          Maintained Amazon amplify apps to create and deploy React web applications for companies such as <a href="https://brooklynnets.everlywell.com/">NBA</a>, <a href="https://tinder.everlywell.com/">Tinder</a>, and other companies for COVID-19 at-home test kits.</li>
-        <li>
-          Implemented a script that helps accurately access and refund unused covid test kits; helping company save up to 200,000 USD.</li>
-        <li>
-          Created several Rails controllers for internal purposes; mocking end to end user experience for QA, mass refund features for CX department, and more, ultimately reducing support tickets amount by 50 percent.</li>
-        <li>
-          Implemented an audit table to help debug problems and logged which process was responsible for the change of the record using PaperTrail gems</li>
-      </ul>
-
-      <div class="flex-box">
-        <p class="entry-title-style">
-          <a href="https://www.spiria.com/">Spiria</a>
-        </p>
-        <p class="entry-location-style">Oakville, ON, Canada</p>
-      </div>
-      <div class="flex-box">
-        <p class="entry-position-style">SOFTWARE ENGINEER</p>
-        <p class="entry-date-style">October, 2018 - October, 2020</p>
-      </div>
-      <ul class="description-style">
-        <li>
-          Constructed RESTful API endpoints in multiple different frameworks such as Django, Ruby on Rails, and Flask and automated API documentation process using swagger.
-        </li>
-        <li>
-          Designed custom rake tasks for importing production data into newly updated data structure to meet client's needs.
-        </li>
-        <li>
-          Maintained or updated staging/productions servers. Debugged problems in production postgres database using ssh and postgres console on Heroku or AWS servers
-        </li>
-        <li>
-          Collaborated in creating automation python scripts for websites and application using selenium covering for QA eliminating 80% of QA's manual work
-        </li>
-      </ul>
-
-      <div class="flex-box">
-        <p class="entry-title-style">
-          <a href="https://www.apexscore.ai/">Apex Score</a>
-        </p>
-        <p class="entry-location-style">Oakville, ON, Canada</p>
-      </div>
-      <div class="flex-box">
-        <p class="entry-position-style">SOFTWARE ENGINEER</p>
-        <p class="entry-date-style">September, 2019 - October, 2020</p>
-      </div>
-      <ul class="description-style">
-        <li>
-          Developed custom Shapley value regression model to calculate importance of independent variables of data sets using sklearn, pandas, and numpy.
-        </li>
-        <li>
-          Created custom image uploader to Amazon s3 bucket using boto3 library.
-        </li>
-        <li>
-          Built RESTful API application using Flask framework and automated extensive API documentation pages using flask-restplus, pytest, and swagger, covering 95% of the code base.
-        </li>
-        <li>
-          Created an interactive graph using D3.js in Vue.js with data from Flask backend API. 
-        </li>
-      </ul>
-
-     <div class="sub-header">
-        <h2 class="section-style"> Education </h2>
-        <div class="line"></div>
-      </div>
-      <div class="flex-box">
-        <p class="entry-title-style">
-          University of British Columbia
-        </p>
-        <p class="entry-location-style">Kelowna, British Columbia</p>
-      </div>
-      <div class="flex-box">
-        <p class="entry-position-style">BACHELOR OF SCIENCE IN PHYSICS</p>
-        <p class="entry-date-style">2014 - 2018</p>
-      </div>
-      <div id="footer"></div>
-    </main>
-    <div style="display: flex; align-items: center; justify-content: center; margin: 30px 0px;">
-  <small>&copy; 2026 June Park</small>
-</div>
-
-    <script href="index.js"></script>
-  </body>
-</htmlLocation: /tools/markdown_to_html
-
--- a/mrjunejune/test/theme_and_webp_test.js	Tue Aug 04 15:12:18 2026 -0700
+++ b/mrjunejune/test/theme_and_webp_test.js	Tue Aug 04 16:49:01 2026 -0700
@@ -110,10 +110,35 @@
         data[index + 1] * 0.7152 +
         data[index + 2] * 0.0722;
     }
+    const channels = value => value.match(/\d+(?:\.\d+)?/g)
+      .slice(0, 3)
+      .map(Number);
+    const colorLuminance = value => {
+      const converted = channels(value).map(channel => {
+        const normalized = channel / 255;
+        return normalized <= 0.04045
+          ? normalized / 12.92
+          : ((normalized + 0.055) / 1.055) ** 2.4;
+      });
+      return converted[0] * 0.2126 +
+        converted[1] * 0.7152 +
+        converted[2] * 0.0722;
+    };
+    const bodyStyle = getComputedStyle(document.body);
+    const foreground = colorLuminance(bodyStyle.color);
+    const background = colorLuminance(
+      getComputedStyle(document.querySelector('main')).backgroundColor,
+    );
     return {
+      cardCount: document.querySelectorAll('.site-link-grid zen-card').length,
+      componentReady: Boolean(customElements.get('zen-card')),
       count,
+      fontFamily: getComputedStyle(document.body).fontFamily,
       luminance: count ? luminance / count : 0,
-      rootClass: document.documentElement.className,
+      textContrast: (Math.max(foreground, background) + 0.05) /
+        (Math.min(foreground, background) + 0.05),
+      rootTheme: document.documentElement.dataset.zenTheme || 'auto',
+      themeLabel: document.querySelector('#themeName')?.textContent,
     };
   });
 
@@ -122,6 +147,27 @@
   return sample;
 }
 
+async function testThemeCycle(browser) {
+  const context = await browser.newContext({ colorScheme: 'light' });
+  const page = await context.newPage();
+  await page.goto(baseUrl, { waitUntil: 'networkidle' });
+  const expected = ['paper', 'ink', 'playful', 'auto'];
+  for (const theme of expected) {
+    await page.locator('#themeToggle').click();
+    assert.equal(
+      await page.evaluate(() =>
+        document.documentElement.dataset.zenTheme || 'auto'
+      ),
+      theme,
+    );
+    assert.equal(
+      await page.evaluate(() => localStorage.getItem('theme-preference')),
+      theme,
+    );
+  }
+  await context.close();
+}
+
 async function testHlsPlayer(browser, siteRoot) {
   const page = await browser.newPage();
   const errors = [];
@@ -395,12 +441,18 @@
       await fetch(`${baseUrl}/public/manifest.json`)
     ).text();
     const serviceWorker = await (
-      await fetch(`${baseUrl}/public/sw.js`)
+      await fetch(`${baseUrl}/sw.js`)
     ).text();
     for (const source of [home, dogGame, manifest]) {
       assert.doesNotMatch(source, /\.png(?:["')]|$)/i);
     }
-    assert.match(serviceWorker, /v4-hlsjs/);
+    assert.match(serviceWorker, /v5-zenbu-themes/);
+    assert.match(
+      await (
+        await fetch(`${baseUrl}/public/pwa-register.js`)
+      ).text(),
+      /register\('\/sw\.js', \{ scope: '\/' \}\)/,
+    );
     assert.ok(serviceWorker.includes("startsWith('/tools/hls_player')"));
 
     for (const asset of [
@@ -420,12 +472,29 @@
       headless: true,
       args: ['--no-sandbox'],
     });
-    const light = await sampleTheme(browser, 'light');
-    const dark = await sampleTheme(browser, 'dark');
-    assert.ok(light.count > 0);
-    assert.ok(dark.count > 0);
-    assert.ok(light.luminance < 175, JSON.stringify(light));
-    assert.ok(dark.luminance > 200, JSON.stringify(dark));
+    const paper = await sampleTheme(browser, 'paper');
+    const ink = await sampleTheme(browser, 'ink');
+    const playful = await sampleTheme(browser, 'playful');
+    const automatic = await sampleTheme(browser, 'auto');
+    assert.equal(paper.rootTheme, 'paper');
+    assert.equal(ink.rootTheme, 'ink');
+    assert.equal(playful.rootTheme, 'playful');
+    assert.equal(automatic.rootTheme, 'auto');
+    assert.equal(paper.themeLabel, 'Paper');
+    assert.equal(ink.themeLabel, 'Ink');
+    assert.equal(playful.themeLabel, 'Playful');
+    assert.equal(automatic.themeLabel, 'Auto');
+    for (const sample of [paper, ink, playful, automatic]) {
+      assert.ok(sample.count > 0);
+      assert.equal(sample.componentReady, true);
+      assert.equal(sample.cardCount, 4);
+      assert.match(sample.fontFamily, /More/);
+      assert.ok(sample.textContrast >= 4.5, JSON.stringify(sample));
+    }
+    assert.ok(paper.luminance < 175, JSON.stringify(paper));
+    assert.ok(playful.luminance < 175, JSON.stringify(playful));
+    assert.ok(ink.luminance > 200, JSON.stringify(ink));
+    await testThemeCycle(browser);
     await testHlsPlayer(browser, siteRoot);
   } finally {
     if (browser) await browser.close();