openvibers/openvibe.tools Commits

Themes: sync from account without a token gate, cache custom themes, kill flash

OP OpenVibers committed 18 Aug 2026 · 27573b6309 · 47 files, +401 −36

The theme could silently "disappear" and pages flashed the default purple.
Three causes, all fixed here:

1. The account sync only ran when an ov_token cookie was readable from JS.
   On sites where it is not, the sync never ran at all, so the account theme
   never arrived and the local cache (or the default) won. It now always
   attempts the fetch and lets the cookie ride along via credentials:include
   — openvibe.network already allows credentialed CORS per origin. A Bearer
   header is still sent when a token happens to be readable.

2. It ran on a 500ms timer. The cached theme is already painted by then, so
   the delay bought nothing and only widened the window where a cold or stale
   cache showed the wrong colours. It now runs immediately.

3. Any theme id not in the hardcoded built-in map — every community/custom
   theme — failed applyById and fell through to the default. Unknown ids are
   now resolved once from the catalog and cached with their full variable set,
   so the next load paints them synchronously.

save() now also PUTs to /api/themes/me so picking a theme anywhere persists to
the account and follows the user across sites; it takes opts.custom for
per-user overrides and opts.sync=false to stay device-local. The server sync
path writes through cacheLocally() rather than save() to avoid a write-back
loop. Every cache write now stores the full variable set and stamps
data-theme on <html>.

Plus an inline critical bootstrap on every themed page, above the stylesheets,
that applies the cached variables from localStorage before first paint.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Xw6Sf5zzSALw8m93XNu5JE

Changes

ddiff --git a/apps/audio/public/index.html b/apps/audio/public/index.html
iindex 2c3bea5..517737e 100644
---- a/apps/audio/public/index.html
++++ b/apps/audio/public/index.html
@@@ -9,6 +9,13 @@
<meta property="og:type" content="website">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/style.css">
ddiff --git a/apps/docs/public/index.html b/apps/docs/public/index.html
iindex f329746..30fc866 100644
---- a/apps/docs/public/index.html
++++ b/apps/docs/public/index.html
@@@ -9,6 +9,13 @@
<meta property="og:type" content="website">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/style.css">
ddiff --git a/apps/food/public/index.html b/apps/food/public/index.html
iindex 29be225..781f302 100644
---- a/apps/food/public/index.html
++++ b/apps/food/public/index.html
@@@ -7,6 +7,13 @@
<meta name="description" content="Budget grocery finder, meal planning optimizer, food bank locator, and nutrition database for survival on a budget.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css">
ddiff --git a/apps/gateway/public/audio.html b/apps/gateway/public/audio.html
iindex 8fbe5cc..c581414 100644
---- a/apps/gateway/public/audio.html
++++ b/apps/gateway/public/audio.html
@@@ -12,6 +12,13 @@
<link rel="canonical" href="https://audio.openvibe.tools">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
ddiff --git a/apps/gateway/public/dev.html b/apps/gateway/public/dev.html
iindex 35b6b79..510834e 100644
---- a/apps/gateway/public/dev.html
++++ b/apps/gateway/public/dev.html
@@@ -12,6 +12,13 @@
<link rel="canonical" href="https://dev.openvibe.tools">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
ddiff --git a/apps/gateway/public/img.html b/apps/gateway/public/img.html
iindex 15d1441..4dc75f5 100644
---- a/apps/gateway/public/img.html
++++ b/apps/gateway/public/img.html
@@@ -12,6 +12,13 @@
<link rel="canonical" href="https://img.openvibe.tools">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
ddiff --git a/apps/gateway/public/index.html b/apps/gateway/public/index.html
iindex 8c1ec22..2816cf1 100644
---- a/apps/gateway/public/index.html
++++ b/apps/gateway/public/index.html
@@@ -28,6 +28,13 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
ddiff --git a/apps/gateway/public/net.html b/apps/gateway/public/net.html
iindex 6cfe62a..099a66c 100644
---- a/apps/gateway/public/net.html
++++ b/apps/gateway/public/net.html
@@@ -12,6 +12,13 @@
<link rel="canonical" href="https://net.openvibe.tools">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
ddiff --git a/apps/gateway/public/paste.html b/apps/gateway/public/paste.html
iindex 1d3ed69..7777709 100644
---- a/apps/gateway/public/paste.html
++++ b/apps/gateway/public/paste.html
@@@ -12,6 +12,13 @@
<link rel="canonical" href="https://pastes.openvibe.tools">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
ddiff --git a/apps/img/public/index.html b/apps/img/public/index.html
iindex 489caa3..8b678a6 100644
---- a/apps/img/public/index.html
++++ b/apps/img/public/index.html
@@@ -9,6 +9,13 @@
<meta property="og:type" content="website">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/style.css">
ddiff --git a/apps/maps/public/index.html b/apps/maps/public/index.html
iindex c24e348..20e10b2 100644
---- a/apps/maps/public/index.html
++++ b/apps/maps/public/index.html
@@@ -9,6 +9,13 @@
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Font Awesome 6 -->
ddiff --git a/apps/text/public/ascii.html b/apps/text/public/ascii.html
iindex 8348827..7ec6950 100644
---- a/apps/text/public/ascii.html
++++ b/apps/text/public/ascii.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/badge.html b/apps/text/public/badge.html
iindex 2961383..1861a49 100644
---- a/apps/text/public/badge.html
++++ b/apps/text/public/badge.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/binary.html b/apps/text/public/binary.html
iindex 278e7df..77d3a42 100644
---- a/apps/text/public/binary.html
++++ b/apps/text/public/binary.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/bio.html b/apps/text/public/bio.html
iindex d09e315..f7a6044 100644
---- a/apps/text/public/bio.html
++++ b/apps/text/public/bio.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/braille.html b/apps/text/public/braille.html
iindex 959e8bb..16cf73f 100644
---- a/apps/text/public/braille.html
++++ b/apps/text/public/braille.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/bubble.html b/apps/text/public/bubble.html
iindex 161b9a4..9d8026c 100644
---- a/apps/text/public/bubble.html
++++ b/apps/text/public/bubble.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/case.html b/apps/text/public/case.html
iindex 03e50d7..6c5090c 100644
---- a/apps/text/public/case.html
++++ b/apps/text/public/case.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/clean.html b/apps/text/public/clean.html
iindex b6d713f..67a052d 100644
---- a/apps/text/public/clean.html
++++ b/apps/text/public/clean.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/compare.html b/apps/text/public/compare.html
iindex cd9e093..020c1cf 100644
---- a/apps/text/public/compare.html
++++ b/apps/text/public/compare.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/count.html b/apps/text/public/count.html
iindex 0b05ba9..a9479ba 100644
---- a/apps/text/public/count.html
++++ b/apps/text/public/count.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/escape.html b/apps/text/public/escape.html
iindex 827225e..de459c7 100644
---- a/apps/text/public/escape.html
++++ b/apps/text/public/escape.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/fancy.html b/apps/text/public/fancy.html
iindex 591cfa1..63906be 100644
---- a/apps/text/public/fancy.html
++++ b/apps/text/public/fancy.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/ice.html b/apps/text/public/ice.html
iindex d8077ee..53067ac 100644
---- a/apps/text/public/ice.html
++++ b/apps/text/public/ice.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/index.html b/apps/text/public/index.html
iindex 54be621..b88f065 100644
---- a/apps/text/public/index.html
++++ b/apps/text/public/index.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/json.html b/apps/text/public/json.html
iindex 9812ec9..b33cbf7 100644
---- a/apps/text/public/json.html
++++ b/apps/text/public/json.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/kaomoji.html b/apps/text/public/kaomoji.html
iindex 8ad2df6..1c6a454 100644
---- a/apps/text/public/kaomoji.html
++++ b/apps/text/public/kaomoji.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/logo-hub.html b/apps/text/public/logo-hub.html
iindex 472c18c..fbf34fc 100644
---- a/apps/text/public/logo-hub.html
++++ b/apps/text/public/logo-hub.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/markdown.html b/apps/text/public/markdown.html
iindex 7aba5c4..2e69a91 100644
---- a/apps/text/public/markdown.html
++++ b/apps/text/public/markdown.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/morse.html b/apps/text/public/morse.html
iindex fbf672e..35d5320 100644
---- a/apps/text/public/morse.html
++++ b/apps/text/public/morse.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/nickname.html b/apps/text/public/nickname.html
iindex a17ddc5..3c44b51 100644
---- a/apps/text/public/nickname.html
++++ b/apps/text/public/nickname.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/peso.html b/apps/text/public/peso.html
iindex 7b40471..e5c8825 100644
---- a/apps/text/public/peso.html
++++ b/apps/text/public/peso.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/reverse.html b/apps/text/public/reverse.html
iindex d30aa3f..30e87fa 100644
---- a/apps/text/public/reverse.html
++++ b/apps/text/public/reverse.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/slang.html b/apps/text/public/slang.html
iindex cf067e9..fd331ef 100644
---- a/apps/text/public/slang.html
++++ b/apps/text/public/slang.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/slug.html b/apps/text/public/slug.html
iindex be5ef57..65ba264 100644
---- a/apps/text/public/slug.html
++++ b/apps/text/public/slug.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/sort.html b/apps/text/public/sort.html
iindex b82feaa..d6ef44f 100644
---- a/apps/text/public/sort.html
++++ b/apps/text/public/sort.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/spanish.html b/apps/text/public/spanish.html
iindex 9290dd6..6e936d9 100644
---- a/apps/text/public/spanish.html
++++ b/apps/text/public/spanish.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/symbols.html b/apps/text/public/symbols.html
iindex 42b33d1..e34e734 100644
---- a/apps/text/public/symbols.html
++++ b/apps/text/public/symbols.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/thumbnail.html b/apps/text/public/thumbnail.html
iindex fbd6112..52a4d49 100644
---- a/apps/text/public/thumbnail.html
++++ b/apps/text/public/thumbnail.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/title.html b/apps/text/public/title.html
iindex c4f243e..e136416 100644
---- a/apps/text/public/title.html
++++ b/apps/text/public/title.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/transparent.html b/apps/text/public/transparent.html
iindex 858d626..cdeee95 100644
---- a/apps/text/public/transparent.html
++++ b/apps/text/public/transparent.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/unicode.html b/apps/text/public/unicode.html
iindex 4cf294b..affdabf 100644
---- a/apps/text/public/unicode.html
++++ b/apps/text/public/unicode.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/watermark.html b/apps/text/public/watermark.html
iindex 250b99a..041abf8 100644
---- a/apps/text/public/watermark.html
++++ b/apps/text/public/watermark.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/wordmark.html b/apps/text/public/wordmark.html
iindex 9087368..ba57edb 100644
---- a/apps/text/public/wordmark.html
++++ b/apps/text/public/wordmark.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/text/public/zalgo.html b/apps/text/public/zalgo.html
iindex 7f8c66d..b674ebc 100644
---- a/apps/text/public/zalgo.html
++++ b/apps/text/public/zalgo.html
@@@ -8,6 +8,13 @@
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
ddiff --git a/apps/yt/public/index.html b/apps/yt/public/index.html
iindex 61ce867..439cabe 100644
---- a/apps/yt/public/index.html
++++ b/apps/yt/public/index.html
@@@ -7,6 +7,13 @@
<meta name="description" content="Download YouTube videos and audio for free. MP4, WebM, MP3, M4A, OPUS, FLAC. No sign-up required.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
++<!-- Critical theme bootstrap (ov-theme-boot) — MUST stay inline and above the stylesheets.
++ Paints the cached theme synchronously so the default palette never flashes. Reads
++ localStorage only: no network, nothing to block on. theme-loader.js still runs after
++ and reconciles with the account. -->
++<script>
++(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
++</script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/style.css">
ddiff --git a/vendor/openvibe-shared/theme-loader.js b/vendor/openvibe-shared/theme-loader.js
iindex 6084562..9db74b5 100644
---- a/vendor/openvibe-shared/theme-loader.js
++++ b/vendor/openvibe-shared/theme-loader.js
@@@ -117,7 +117,9 @@
const saved = JSON.parse(raw);
if (saved && saved.variables && typeof saved.variables === 'object') {
applyVars(saved.variables);
-- return saved.id || saved.slug || 'vibe';
++ const cachedId = saved.id || saved.slug || 'vibe';
++ try { document.documentElement.setAttribute('data-theme', cachedId); } catch { /* no DOM */ }
++ return cachedId;
}
if (saved && (saved.id || saved.slug)) {
const id = saved.id || saved.slug;
@@@ -129,10 +131,8 @@
// 2. Try cross-domain cookie
const cookieId = getCookie(COOKIE_NAME);
if (cookieId && applyById(cookieId)) {
-- // Cache in localStorage for faster next load
-- try {
-- localStorage.setItem(STORAGE_KEY, JSON.stringify({ id: cookieId, variables: THEMES[cookieId] }));
-- } catch { /* quota */ }
++ // Cache the full variable set so the next load paints from localStorage instantly.
++ cacheLocally(cookieId, THEMES[cookieId]);
return cookieId;
}
@@@ -141,17 +141,47 @@
return 'vibe';
}
++ function authToken() {
++ return getCookie('ov_token') ||
++ (typeof localStorage !== 'undefined' && (localStorage.getItem('ov_token') || localStorage.getItem('token'))) ||
++ null;
++ }
++
/**
-- * Save theme choice both locally and cross-domain.
-- * Call this when the user picks a theme.
++ * Write the resolved theme to every local cache. Always stores the FULL variable set —
++ * including for community/custom themes that aren't in the built-in map — so the next
++ * page load can paint the right colours synchronously with no network call and no flash.
*/
-- function save(themeId, variables) {
-- const vars = variables || THEMES[themeId];
-- if (!vars) return;
++ function cacheLocally(themeId, vars) {
try {
-- localStorage.setItem(STORAGE_KEY, JSON.stringify({ id: themeId, slug: themeId, variables: vars }));
++ localStorage.setItem(STORAGE_KEY, JSON.stringify({
++ id: themeId, slug: themeId, variables: vars, at: Date.now(),
++ }));
} catch { /* quota */ }
setCookie(COOKIE_NAME, themeId, 365);
++ try { document.documentElement.setAttribute('data-theme', themeId); } catch { /* no DOM */ }
++ }
++
++ /**
++ * Save theme choice locally AND to the user's openvibe.network account, so the choice
++ * follows them to every OpenVibe site. Pass opts.custom for per-user variable overrides;
++ * opts.sync === false keeps it device-local.
++ */
++ function save(themeId, variables, opts) {
++ const custom = (opts && opts.custom) || null;
++ const base = variables || THEMES[themeId] || {};
++ const vars = custom ? Object.assign({}, base, custom) : base;
++ if (!vars || !Object.keys(vars).length) return;
++ cacheLocally(themeId, vars);
++ if (opts && opts.sync === false) return;
++ if (typeof fetch === 'undefined') return;
++ const token = authToken();
++ const headers = { 'Content-Type': 'application/json' };
++ if (token) headers['Authorization'] = 'Bearer ' + token;
++ fetch(_config.apiBase + '/api/themes/me', {
++ method: 'PUT', headers: headers, credentials: 'include',
++ body: JSON.stringify({ theme_id: themeId, custom_variables: custom }),
++ }).catch(function () { /* offline / signed out — local cache still holds */ });
}
/**
@@@ -171,34 +201,47 @@
// ── Auto-apply on load (synchronous, prevents FOUC) ─────
const activeId = resolveAndApply();
-- // ── If user is logged in, try to sync from server ────────
-- // This runs async after page paint to pick up server-side changes
-- if (typeof fetch !== 'undefined') {
-- setTimeout(function () {
-- const token = getCookie('ov_token') ||
-- (typeof localStorage !== 'undefined' && (localStorage.getItem('ov_token') || localStorage.getItem('token')));
-- if (!token) return;
-- fetch(_config.apiBase + '/api/themes/me/active', {
-- headers: { 'Authorization': 'Bearer ' + token },
-- credentials: 'include',
-- }).then(function (r) { return r.ok ? r.json() : null; })
-- .then(function (data) {
++ // Look up a theme we don't ship in the built-in map (community/custom themes) from the
++ // central catalog. Without this, any non-built-in id fell through to the default 'vibe'
++ // — which is why a custom theme could "disappear" and flash purple on every load.
++ function fetchThemeVars(themeId) {
++ return fetch(_config.apiBase + '/api/themes/' + encodeURIComponent(themeId))
++ .then(function (r) { return r.ok ? r.json() : null; })
++ .then(function (d) { return (d && d.theme && d.theme.variables) || null; })
++ .catch(function () { return null; });
++ }
++
++ // ── Pull the account's theme from openvibe.network ───────
++ // Runs IMMEDIATELY, not on a 500ms timer: the cached theme is already painted, so this is
++ // a silent correction — delaying it only widened the window where a cold or stale cache
++ // showed the wrong theme. There is also no token gate any more; the ov_token cookie rides
++ // along via credentials:'include' (openvibe.network allows credentialed CORS per origin),
++ // so this now works on sites where the cookie isn't readable from JS — previously the sync
++ // silently never ran there and the user's theme appeared to be "lost".
++ function syncFromServer() {
++ if (typeof fetch === 'undefined') return;
++ const token = authToken();
++ const headers = token ? { 'Authorization': 'Bearer ' + token } : {};
++ fetch(_config.apiBase + '/api/themes/me/active', { headers: headers, credentials: 'include' })
++ .then(function (r) { return r.ok ? r.json() : null; })
++ .then(function (data) {
if (!data || !data.theme_id) return;
const serverId = data.theme_id;
-- const custom = data.custom_variables;
-- if (custom && typeof custom === 'object' && Object.keys(custom).length) {
-- // Custom theme: apply the built-in base (if any) then the user's overrides.
-- const base = THEMES[serverId] || {};
-- applyVars(base);
-- applyVars(custom);
-- try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ id: serverId, variables: Object.assign({}, base, custom) })); } catch { /* quota */ }
-- setCookie(COOKIE_NAME, serverId, 365);
-- } else if (serverId !== getCurrent()) {
-- if (applyById(serverId)) save(serverId, THEMES[serverId]);
-- }
-- }).catch(function () { /* offline / CORS */ });
-- }, 500);
++ const custom = (data.custom_variables && typeof data.custom_variables === 'object'
++ && Object.keys(data.custom_variables).length) ? data.custom_variables : null;
++ const builtin = THEMES[serverId];
++ const settle = function (base) {
++ const vars = custom ? Object.assign({}, base || {}, custom) : (base || null);
++ if (!vars || !Object.keys(vars).length) return;
++ applyVars(vars);
++ cacheLocally(serverId, vars); // cacheLocally, not save() — no write-back loop
++ };
++ if (builtin) return settle(builtin);
++ fetchThemeVars(serverId).then(settle);
++ })
++ .catch(function () { /* offline / signed out — keep the cached theme */ });
}
++ syncFromServer();
// ── Public API ───────────────────────────────────────────
const OpenVibeThemeLoader = {