// OpenVibe — Theme Loader (browser): the person's look on before first paint. Load it before navbar.js.
(function (root) {
'use strict';
const COOKIE_NAME = 'ov_theme_id';
const STORAGE_KEY = 'ov_theme';
// The account's look lives on openvibe.network for every site (init({ apiBase }) overrides it).
const DEFAULT_API_BASE = 'https://openvibe.network';
let _config = { apiBase: DEFAULT_API_BASE };
// ── Condensed built-in theme variable maps ───────────────
// Kept in sync with packages/openvibe-shared/builtin-themes.js
// THEMES:BEGIN — generated by scripts/build-theme-loader.js from builtin-themes.js; do not edit by hand
const THEME_KEYS = ['--bg-primary','--bg-secondary','--bg-tertiary','--bg-card','--bg-hover','--bg-input','--text-primary','--text-secondary','--text-muted','--accent','--accent-light','--accent-dark','--border','--border-light','--live-red','--success','--warning','--danger','--info','--shadow','--shadow-lg','--success-text','--warning-text','--danger-text','--info-text','--live-red-text','--accent-strong','--on-accent-strong','--on-accent','--accent-rgb','--accent-glow','--color-scheme'];
const THEME_ROWS = {
'vibe': ['#0a0f1c','#101828','#172238','#131c2e','#1c2a44','#0d1424','#e6edf7','#96a7c2','#8192ac','#3b82f6','#60a5fa','#1d4ed8','#1f2d47','#2c3d5c','#ef4444','#22c55e','#f59e0b','#ef4444','#38bdf8','0 2px 12px rgba(0,0,0,0.45)','0 8px 32px rgba(0,0,0,0.6)','#22c55e','#f59e0b','#f26262','#38bdf8','#f26262','#3472d8','#ffffff','#ffffff','59,130,246','rgba(59,130,246,0.35)','dark'],
'violet-hour': ['#0d0d0f','#16161a','#1e1e24','#1a1a20','#242430','#12121a','#e8e6e3','#9a9a9a','#8d8c8c','#8b5cf6','#a78bfa','#6d28d9','#2a2a32','#3a3a44','#e74c3c','#2ecc71','#f39c12','#e74c3c','#3498db','0 2px 12px rgba(0,0,0,0.4)','','#2ecc71','#f39c12','#e95e50','#3498db','#e95e50','#8558ec','','','139,92,246','rgba(139,92,246,0.35)'],
'midnight': ['#0a0e17','#111827','#1a2235','#151d2e','#1e2a42','#0d1220','#e2e8f0','#94a3b8','#8693a5','#6366f1','#818cf8','#4f46e5','#1e293b','#334155','','','','','','','','','','','','','#6164ec','','','99,102,241','rgba(99,102,241,0.35)'],
'forest': ['#0a120a','#121e12','#1a2a1a','#152015','#1e3420','#0d160d','#d4e8d4','#8aaa8a','#849e84','#4ade80','#86efac','#22c55e','#1a3a1a','#2a4a2a','','','','','','','','','','#f36d6d','','#f36d6d','#4ade80','#0b0d10','#0b0d10','74,222,128','rgba(74,222,128,0.35)'],
'neon-tokyo': ['#0a0a0f','#12101a','#1a1625','#15111f','#221a32','#0d0b14','#f0e6ff','#b4a0d0','#8e81a2','#ff006e','#ff4d94','#c9005a','#2a1a3a','#3a2a4a','','','','','','','','','','#ef4848','','#ef4848','#e60063','','','255,0,110','rgba(255,0,110,0.35)'],
'dracula': ['#282a36','#1e1f29','#343746','#2c2e3c','#3a3d50','#21222c','#f8f8f2','#bfbfb2','#9ea8c5','#bd93f9','#d4b8ff','#9b6ddb','#44475a','#565970','#ff5555','#50fa7b','#f1fa8c','#ff5555','#8be9fd','','','#50fa7b','#f1fa8c','#ff8585','#8be9fd','#ff8585','#bd93f9','#0b0d10','#0b0d10','189,147,249','rgba(189,147,249,0.35)'],
'monokai': ['#272822','#1e1f1c','#3e3d32','#2d2e27','#49483e','#22231e','#f8f8f2','#cfcfc2','#b8b6ab','#a6e22e','#c4ff50','#7dac1c','#49483e','#5b5a50','','','#e6db74','#f92672','#66d9ef','','','#45ce78','#e6db74','#fc97bb','#66d9ef','#f79e9e','#a6e22e','#0b0d10','#0b0d10','166,226,46','rgba(166,226,46,0.35)'],
'ocean-deep': ['#0a1215','#0f1a1f','#162228','#121c22','#1a2a32','#0c1418','#d0e8ef','#7aa8ba','#7494a1','#00bcd4','#4dd0e1','#0097a7','#1a3040','#2a4050','','','','','','','','','','#f15e5e','','#f15e5e','#00bcd4','#0b0d10','#0b0d10','0,188,212','rgba(0,188,212,0.35)'],
'sunset': ['#120a0a','#1a1010','#241616','#1e1212','#2e1a1a','#140c0c','#ffe8e0','#c49080','#9d8179','#ff6b35','#ff8f5e','#d04a1a','#3a2020','#4a3030','','','','','','','','','','#f04b4b','','#f04b4b','#ff6b35','#0b0d10','#0b0d10','255,107,53','rgba(255,107,53,0.35)'],
'arctic': ['#0b1620','#0f1e2b','#163040','#122534','#1a3a4c','#0c1a26','#eaf6ff','#9ec3d8','#81a5bb','#22d3ee','#a5f3fc','#0891b2','#1e3f52','#2a5268','','#34d399','','','#7dd3fc','0 2px 12px rgba(2,12,20,0.5)','0 8px 32px rgba(2,12,20,0.65)','#34d399','','#f47c7c','#7dd3fc','#f47c7c','#22d3ee','#0b0d10','#0b0d10','34,211,238','rgba(34,211,238,0.35)'],
'ember': ['#120808','#1a0e0e','#241414','#1e1010','#2e1818','#140a0a','#f0dada','#b0808a','#9b8084','#ef4444','#f87171','#b91c1c','#3a1a20','#4a2a30','','','','','','','','','','#ef4848','','#ef4848','#d73d3d','','','239,68,68','rgba(239,68,68,0.35)'],
'vapor': ['#0e0a14','#14101c','#1c1628','#181220','#241a34','#100c16','#f0e0ff','#c090e0','#967bbb','#e040fb','#ea80fc','#aa00d4','#2a1a40','#3a2a50','','#69f0ae','','','#00e5ff','','','#69f0ae','','#f04b4b','#00e5ff','#f04b4b','#bc36d3','','','224,64,251','rgba(224,64,251,0.35)'],
'slate': ['#0f1118','#161820','#1e2028','#1a1c24','#24262e','#12141c','#e4e6ea','#a0a4b0','#8a8d97','#a78bfa','#c4b5fd','#7c3aed','#2a2c38','#3a3c48','','','','','','','','','','#f15a5a','','#f15a5a','#a78bfa','#0b0d10','#0b0d10','167,139,250','rgba(167,139,250,0.35)'],
'matrix': ['#000000','#050a05','#0a140a','#071007','#0e1e0e','#030803','#00ff41','#00b030','#149836','#00ff41','#66ff8a','#00aa2a','#0a2a0a','#144014','','','','','','','','','','#ef4444','','#ef4444','#00ff41','#0b0d10','#0b0d10','0,255,65','rgba(0,255,65,0.35)'],
'nord': ['#2e3440','#272c36','#3b4252','#333a48','#434c5e','#2a303c','#eceff4','#d8dee9','#b5bcca','#88c0d0','#8fbcbb','#5e81ac','#3b4252','#4c566a','','#a3be8c','#ebcb8b','#bf616a','#81a1c1','','','#acc598','#ebcb8b','#dfb0b5','#a9bfd5','#f8a9a9','#88c0d0','#0b0d10','#0b0d10','136,192,208','rgba(136,192,208,0.35)'],
'gruvbox-dark': ['#1d2021','#282828','#3c3836','#32302f','#504945','#242424','#ebdbb2','#d5c4a1','#c2b8aa','#fabd2f','#fce566','#d79921','#3c3836','#504945','','#b8bb26','#fe8019','#fb4934','#83a598','','','#bec037','#fea65e','#fda49a','#a8c0b7','#f7a5a5','#fabd2f','#0b0d10','#0b0d10','250,189,47','rgba(250,189,47,0.35)'],
'abyss': ['#000000','#080808','#111111','#0a0a0a','#181818','#050505','#cccccc','#888888','#828282','#ffffff','#ffffff','#aaaaaa','#1a1a1a','#252525','','','','','','','','','','#ef4444','','#ef4444','#ffffff','#0b0d10','#0b0d10','255,255,255','rgba(255,255,255,0.35)'],
'copper': ['#110d09','#1a140e','#231c14','#1e1710','#2c2218','#13100b','#e8ddd0','#b49a80','#978976','#cd7f32','#daa060','#a06020','#2a2016','#3a3020','','','','','','','','','','#f15757','','#f15757','#a46628','','','205,127,50','rgba(205,127,50,0.35)'],
'sakura-night': ['#0e0a14','#150f1c','#1c1526','#181020','#221830','#100c16','#f0e0f0','#c0a0c0','#997d99','#f472b6','#f9a8d4','#db2777','#2a1a30','#3a2a40','','','','','','','','','','#ef4848','','#ef4848','#f472b6','#0b0d10','#0b0d10','244,114,182','rgba(244,114,182,0.35)'],
'hacker': ['#000000','#0a0800','#141000','#0e0c00','#1c1800','#060400','#ffb000','#cc8d00','#a97a0f','#ffb000','#ffd050','#cc8d00','#1a1400','#2a2200','','','','','','','','','','#ef4444','','#ef4444','#ffb000','#0b0d10','#0b0d10','255,176,0','rgba(255,176,0,0.35)'],
'daylight': ['#ffffff','#f7f7f8','#eeeef0','#ffffff','#f0f0f2','#f5f5f7','#1a1a2e','#4a4a6a','#68687b','#7a4ddb','#725faa','#6d28d9','#e0e0e6','#d0d0d8','','','','','','0 2px 12px rgba(0,0,0,0.08)','0 8px 32px rgba(0,0,0,0.12)','#157a3a','#935f07','#c43838','#227195','#c43838','#7a4ddb','','','122,77,219','rgba(122,77,219,0.35)','light'],
'paper': ['#faf8f5','#f0ece6','#e8e2d8','#faf8f5','#f0ece6','#f5f2ed','#2c2416','#5a4e3e','#6c6355','#7d5f12','#7b6122','#6a5010','#ddd6c8','#ccc4b0','','','','','','0 2px 12px rgba(60,40,0,0.06)','0 8px 32px rgba(60,40,0,0.10)','#147237','#895806','#b63434','#1f6a8b','#b63434','#7d5f12','','','125,95,18','rgba(125,95,18,0.35)','light'],
'cloud': ['#f0f5ff','#e8eef8','#dde5f0','#f5f8ff','#e4ecf8','#edf2fa','#1a2040','#4a5070','#5e637b','#245dd5','#3c669b','#2563eb','#d0d8ea','#bcc8e0','','','','','','0 2px 12px rgba(0,20,60,0.06)','0 8px 32px rgba(0,20,60,0.10)','#147237','#895806','#ba3535','#1f6a8b','#ba3535','#245dd5','','','36,93,213','rgba(36,93,213,0.35)','light'],
'meadow': ['#f2f8f0','#e8f0e4','#dce8d6','#f5faf2','#e4f0de','#edf5ea','#1a2e16','#3a5a32','#526b4a','#137538','#147638','#15803d','#c8dcc0','#b0ccaa','','','','','','0 2px 12px rgba(0,40,0,0.06)','0 8px 32px rgba(0,40,0,0.10)','#147638','#895806','#ba3535','#1f6a8b','#ba3535','#137538','','','19,117,56','rgba(19,117,56,0.35)','light'],
'peach': ['#fff5f0','#fbeae2','#f5ddd2','#fff7f2','#fce8dc','#fdf0ea','#2e1a14','#6a4438','#7e5d51','#ad4107','#925523','#ea580c','#f0d0c0','#e4bfab','','','','','','0 2px 12px rgba(60,20,0,0.06)','0 8px 32px rgba(60,20,0,0.10)','#147237','#895806','#b63434','#1f6a8b','#b63434','#ad4107','','','173,65,7','rgba(173,65,7,0.35)','light'],
'lavender': ['#f8f5ff','#f0eaf8','#e6ddf0','#faf7ff','#eee6f8','#f4f0fa','#1e1430','#4a3a60','#6a5d80','#7443d5','#68569b','#7c3aed','#ddd0ee','#ccc0dd','','','','','','0 2px 12px rgba(40,0,60,0.06)','0 8px 32px rgba(40,0,60,0.10)','#147237','#895806','#b63434','#1f6a8b','#b63434','#7443d5','','','116,67,213','rgba(116,67,213,0.35)','light'],
'gruvbox-light': ['#fbf1c7','#f2e5bc','#ebdbb2','#fbf1c7','#f2e5bc','#f5ecc4','#3c3836','#504945','#685e54','#a73e06','#934a0e','#af3a03','#d5c4a1','#bdae93','','#98971a','#d79921','#cc241d','#458588','0 2px 12px rgba(40,30,0,0.08)','0 8px 32px rgba(40,30,0,0.12)','#646411','#7d5913','#bc211b','#36686a','#b13232','#a73e06','','','167,62,6','rgba(167,62,6,0.35)','light'],
'snow': ['#ffffff','#f8fafc','#f1f5f9','#ffffff','#f1f5f9','#f8fafc','#0f172a','#334155','#657185','#0275b0','#23759a','#0284c7','#e2e8f0','#cbd5e1','','','','','','0 2px 12px rgba(0,0,0,0.05)','0 8px 32px rgba(0,0,0,0.08)','#167e3c','#986207','#c93939','#23759a','#c93939','#0275b0','','','2,117,176','rgba(2,117,176,0.35)','light'],
'sand': ['#f8f4ef','#f0e8de','#e8ddd0','#faf6f0','#f0e8de','#f5f0e8','#2e2418','#5a4e3e','#6b6154','#7e5d06','#7a5c12','#8b6508','#ddd0be','#ccc0aa','','','','','','0 2px 12px rgba(60,40,0,0.06)','0 8px 32px rgba(60,40,0,0.10)','#136e35','#895806','#b13232','#1e6686','#b13232','#7e5d06','','','126,93,6','rgba(126,93,6,0.35)','light'],
'rose': ['#fff5f7','#fce7ec','#f5d5de','#fff7f9','#fce0e8','#fdeef2','#2e1420','#6a3850','#81536c','#bf183d','#b02d44','#be123c','#f0c8d4','#e4b0c0','','','','','','0 2px 12px rgba(60,0,20,0.06)','0 8px 32px rgba(60,0,20,0.10)','#136e35','#845506','#b13232','#1e6686','#b13232','#bf183d','','','191,24,61','rgba(191,24,61,0.35)','light'],
'solarized-dark': ['#002b36','#073642','#0a3f4e','#053542','#0d4a58','#01303d','#fdf6e3','#a6b2b2','#a8b2b0','#b58900','#d4a017','#8a6a00','#0a4050','#105060','','#859900','#cb4b16','#dc322f','#268bd2','','','#aab84d','#e4a186','#ee9997','#78b7e3','#f69696','#947000','','','181,137,0','rgba(181,137,0,0.35)'],
'solarized-light': ['#fdf6e3','#eee8d5','#e4ddc6','#fdf6e3','#eee8d5','#f5eedb','#002b36','#51656c','#4e6467','#795d00','#7b5d0d','#8a6a00','#d6ceb5','#c8c0a5','','#859900','#cb4b16','#dc322f','#268bd2','0 2px 12px rgba(0,30,40,0.08)','0 8px 32px rgba(0,30,40,0.12)','#5a6800','#a63e12','#b92a27','#1b6497','#b13232','#795d00','','','121,93,0','rgba(121,93,0,0.35)','light'],
'catppuccin-mocha': ['#1e1e2e','#181825','#313244','#1e1e2e','#313244','#1a1a2c','#cdd6f4','#bac2de','#989caf','#cba6f7','#dcc0ff','#b07de0','#313244','#45475a','#f38ba8','#a6e3a1','#fab387','#f38ba8','#89b4fa','','','#a6e3a1','#fab387','#f38ba8','#89b4fa','#f38ba8','#cba6f7','#0b0d10','#0b0d10','203,166,247','rgba(203,166,247,0.35)'],
'catppuccin-latte': ['#eff1f5','#e6e9ef','#dce0e8','#eff1f5','#dce0e8','#e8ebf0','#4c4f69','#5c5f77','#5f6176','#7d34dc','#7a46c2','#7028d4','#ccd0da','#bcc0cc','#d20f39','#40a02b','#fe640b','#d20f39','#1e66f5','0 2px 12px rgba(0,0,0,0.06)','0 8px 32px rgba(0,0,0,0.10)','#2d701e','#a84207','#c50e36','#1a5ad8','#c50e36','#7d34dc','','','125,52,220','rgba(125,52,220,0.35)','light'],
'high-contrast': ['#000000','#0a0a0a','#1a1a1a','#0f0f0f','#222222','#0a0a0a','#ffffff','#f0f0f0','#aaaaaa','#ffd700','#ffed4a','#ccac00','#444444','#666666','#ff0000','#00ff00','#ffff00','#ff0000','#00ffff','','','#00ff00','#ffff00','#ff3d3d','#00ffff','#ff3d3d','#ffd700','#0b0d10','#0b0d10','255,215,0','rgba(255,215,0,0.35)'],
'halloween': ['','#0f1626','#151e33','#111a2b','#1a2540','#0c1322','#eef1f6','#a9b4c8','#8c99b0','#ff7a1a','#ffa14f','#d65d00','#1c2840','#2a3858','','#4ade80','#fbbf24','#f87171','#60a5fa','0 2px 12px rgba(0,4,14,0.5)','0 8px 32px rgba(0,4,14,0.65)','#4ade80','#fbbf24','#f87171','#60a5fa','#f15757','#ff7a1a','#0b0d10','#0b0d10','255,122,26','rgba(255,122,26,0.35)'],
'frost': ['#08111b','#0d1926','#132334','#0f1c2b','#18293d','#0a1520','#eaf4fb','#a3b9cc','#7f97ad','#2dd4bf','#7ee8da','#0f9e8c','#1b2d41','#294057','','#4ade80','#fbbf24','#f87171','#93c5fd','0 2px 12px rgba(0,6,14,0.5)','0 8px 32px rgba(0,6,14,0.65)','#4ade80','#fbbf24','#f87171','#93c5fd','#f15e5e','#2dd4bf','#0b0d10','#0b0d10','45,212,191','rgba(45,212,191,0.35)'],
'dawn': ['#f5f6fc','#eceefa','#e1e4f5','#fafbff','#e6e9f8','#f0f2fb','#161a33','#474d6b','#5e647f','#4f46e5','#5558cf','#3730a3','#d6daef','#c3c8e6','','','','','','0 2px 12px rgba(10,14,40,0.07)','0 8px 32px rgba(10,14,40,0.12)','#147638','#8e5c06','#ba3535','#1f6a8b','#ba3535','#4f46e5','','','79,70,229','rgba(79,70,229,0.35)','light'],
'tokyo-day': ['#fff6fa','#ffedf4','#fde1ec','#fffafc','#fde7f0','#fff0f6','#24101a','#5c3a4a','#7a5566','#cd0058','#c22261','#9c0043','#f3d3e0','#ebbfd1','','','','','','0 2px 12px rgba(40,0,16,0.07)','0 8px 32px rgba(40,0,16,0.12)','#147638','#8e5c06','#bf3636','#206e90','#bf3636','#cd0058','','','205,0,88','rgba(205,0,88,0.35)','light'],
'alucard': ['#fffbeb','#f6f0d8','#ece4c6','#fffdf3','#f2ead0','#fbf6e2','#1f1f1f','#4f4b38','#6c664b','#644ac9','#6b53bc','#4a33a3','#e3dab8','#d6cba3','','','','','','0 2px 12px rgba(40,30,0,0.07)','0 8px 32px rgba(40,30,0,0.12)','#147237','#895806','#ba3535','#1f6a8b','#ba3535','#644ac9','','','100,74,201','rgba(100,74,201,0.35)','light'],
'monokai-light': ['#fafaf6','#f1f1ea','#e7e7de','#fdfdfa','#ecece3','#f6f6f0','#272822','#4e4f44','#67685c','#4b7100','#537112','#436600','#dcdcd0','#cdcdbf','','','','','','0 2px 12px rgba(20,20,10,0.07)','0 8px 32px rgba(20,20,10,0.12)','#147638','#8e5c06','#ba3535','#206e90','#ba3535','#4b7100','','','75,113,0','rgba(75,113,0,0.35)','light'],
'shallows': ['#f0fafb','#e4f4f6','#d5ecef','#f7fcfd','#dcf0f2','#ebf7f8','#0c2226','#3a5458','#516b70','#007384','#00737f','#005662','#c9e3e7','#b4d6db','','','','','','0 2px 12px rgba(0,30,36,0.07)','0 8px 32px rgba(0,30,36,0.12)','#147638','#8e5c06','#ba3535','#206e90','#ba3535','#007384','','','0,115,132','rgba(0,115,132,0.35)','light'],
'ash': ['#faf7f6','#f3eeec','#eae2df','#fdfbfa','#efe7e4','#f7f3f1','#231716','#54403d','#725b57','#c22727','#bc2f2b','#8e1b1b','#e2d6d2','#d4c4bf','','','','','','0 2px 12px rgba(40,10,8,0.07)','0 8px 32px rgba(40,10,8,0.12)','#147237','#895806','#ba3535','#1f6a8b','#ba3535','#c22727','','','194,39,39','rgba(194,39,39,0.35)','light'],
'vapor-day': ['#fcf6ff','#f6ecfc','#eedff8','#fefaff','#f2e5fa','#f9f1fe','#22122c','#533f5f','#715c7e','#a21caf','#a921ba','#701a75','#e6d3f1','#dbc0ea','','','','','','0 2px 12px rgba(30,0,40,0.07)','0 8px 32px rgba(30,0,40,0.12)','#147237','#895806','#ba3535','#1f6a8b','#ba3535','#a21caf','','','162,28,175','rgba(162,28,175,0.35)','light'],
'greenbar': ['#f4faf3','#e9f4e7','#dcecd9','#f9fdf8','#e2f0df','#eff7ed','#0d1f10','#3b5240','#556e5a','#13782b','#18772c','#0b5a1f','#cfe3cb','#bcd7b7','','','','','','0 2px 12px rgba(0,30,6,0.07)','0 8px 32px rgba(0,30,6,0.12)','#147638','#8e5c06','#ba3535','#206e90','#ba3535','#13782b','','','19,120,43','rgba(19,120,43,0.35)','light'],
'nord-light': ['#eceff4','#e5e9f0','#d8dee9','#f2f4f8','#dfe4ec','#e8ecf2','#2e3440','#434c5e','#4c566a','#476283','#506478','#4c6a91','#d0d7e2','#c2cad8','','','','','','0 2px 12px rgba(20,26,40,0.07)','0 8px 32px rgba(20,26,40,0.12)','#136e35','#845506','#b13232','#1e6686','#b13232','#476283','','','71,98,131','rgba(71,98,131,0.35)','light'],
'bright': ['#ffffff','#f6f6f6','#ececec','#ffffff','#f0f0f0','#fafafa','#0a0a0a','#3d3d3d','#5c5c5c','#111111','#333333','#000000','#e2e2e2','#d0d0d0','','','','','','0 2px 12px rgba(0,0,0,0.07)','0 8px 32px rgba(0,0,0,0.12)','#157a3a','#935f07','#bf3636','#227195','#bf3636','#111111','','','17,17,17','rgba(17,17,17,0.35)','light'],
'high-contrast-light': ['#ffffff','#ffffff','#f2f2f2','#ffffff','#e8e8e8','#ffffff','#000000','#1a1a1a','#2b2b2b','#0000c8','#1a1aff','#00008b','#000000','#333333','','','','','','0 2px 12px rgba(0,0,0,0.07)','0 8px 32px rgba(0,0,0,0.12)','#147638','#8e5c06','#bf3636','#206e90','#bf3636','#0000c8','','','0,0,200','rgba(0,0,200,0.35)','light'],
'hayride': ['#fbf6ee','#f6eee2','#efe3d1','#ffffff','#f3e9da','#fffaf2','#23180f','#5a4838','#6b5a4a','#b63d0b','#ad4109','#9a3412','#eadfcd','#e0cfb6','','','','','','0 2px 12px rgba(40,24,8,0.07)','0 8px 32px rgba(40,24,8,0.12)','#147638','#8e5c06','#ba3535','#1f6a8b','#ba3535','#b63d0b','','','182,61,11','rgba(182,61,11,0.35)','light'],
'snowfall': ['#f2f8fb','#e8f2f7','#dbeaf1','#f8fbfd','#e1eef4','#edf5f9','#0e1d26','#3b5260','#526976','#0f746c','#0c7267','#115e59','#cfe2ea','#bcd5df','','','','','','0 2px 12px rgba(0,20,30,0.07)','0 8px 32px rgba(0,20,30,0.12)','#147638','#8e5c06','#ba3535','#206e90','#ba3535','#0f746c','','','15,116,108','rgba(15,116,108,0.35)','light'],
};
const THEMES = (function () {
const out = {}, base = THEME_ROWS.vibe;
for (const id in THEME_ROWS) {
const row = THEME_ROWS[id], vars = {};
for (let i = 0; i < THEME_KEYS.length; i++) { const v = row[i] || base[i]; if (v) vars[THEME_KEYS[i]] = v; }
out[id] = vars;
}
return out;
})();
const SEASONS = [
{ id: 'halloween', name: 'Halloween', theme: 'halloween', light: 'hayride', from: '10-01', to: '10-31' },
{ id: 'winter', name: 'Winter', theme: 'frost', light: 'snowfall', from: '12-01', to: '12-31' },
];
const PAIRS = { 'vibe':'cloud','violet-hour':'daylight','midnight':'dawn','forest':'meadow','neon-tokyo':'tokyo-day','dracula':'alucard','monokai':'monokai-light','ocean-deep':'shallows','sunset':'peach','arctic':'snow','ember':'ash','vapor':'vapor-day','slate':'lavender','matrix':'greenbar','nord':'nord-light','gruvbox-dark':'gruvbox-light','abyss':'bright','copper':'sand','sakura-night':'rose','hacker':'paper','daylight':'violet-hour','paper':'hacker','cloud':'vibe','meadow':'forest','peach':'sunset','lavender':'slate','gruvbox-light':'gruvbox-dark','snow':'arctic','sand':'copper','rose':'sakura-night','solarized-dark':'solarized-light','solarized-light':'solarized-dark','catppuccin-mocha':'catppuccin-latte','catppuccin-latte':'catppuccin-mocha','high-contrast':'high-contrast-light','halloween':'hayride','frost':'snowfall','dawn':'midnight','tokyo-day':'neon-tokyo','alucard':'dracula','monokai-light':'monokai','shallows':'ocean-deep','ash':'ember','vapor-day':'vapor','greenbar':'matrix','nord-light':'nord','bright':'abyss','high-contrast-light':'high-contrast','hayride':'halloween','snowfall':'frost' };
const MODES = { 'vibe':'dark','violet-hour':'dark','midnight':'dark','forest':'dark','neon-tokyo':'dark','dracula':'dark','monokai':'dark','ocean-deep':'dark','sunset':'dark','arctic':'dark','ember':'dark','vapor':'dark','slate':'dark','matrix':'dark','nord':'dark','gruvbox-dark':'dark','abyss':'dark','copper':'dark','sakura-night':'dark','hacker':'dark','daylight':'light','paper':'light','cloud':'light','meadow':'light','peach':'light','lavender':'light','gruvbox-light':'light','snow':'light','sand':'light','rose':'light','solarized-dark':'dark','solarized-light':'light','catppuccin-mocha':'dark','catppuccin-latte':'light','high-contrast':'dark','halloween':'dark','frost':'dark','dawn':'light','tokyo-day':'light','alucard':'light','monokai-light':'light','shallows':'light','ash':'light','vapor-day':'light','greenbar':'light','nord-light':'light','bright':'light','high-contrast-light':'light','hayride':'light','snowfall':'light' };
// THEMES:END
// ── Seasons: in its dates a season's theme stands in for the default theme unless display `seasonal` is off (a picked
// theme stays). , window 'ov:season'; the mark wears it on any theme (data-ov-season-mark).
const DEFAULT_ID = 'vibe';
function seasonNow(date) {
const d = date || new Date();
const md = String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
for (let i = 0; i < SEASONS.length; i++) {
const s = SEASONS[i];
if (s.from <= s.to ? (md >= s.from && md <= s.to) : (md >= s.from || md <= s.to)) return s;
}
return null;
}
let _season = null;
let _shown = { id: DEFAULT_ID, vars: null, custom: false };
let _trying = null; // { id, name, vars } while a theme is tried on this tab (?ov-try=, 4.41.0)
/** Paint the theme `themeId` (its variables `vars`), or the season's theme in its place. → the id painted. */
// ── Light, dark and Auto: every built-in has two halves (PAIRS); display `scheme` 'auto' paints the half the device
// asks for (a theme without one gives way to the default's half), 'manual' the picked theme as it is.
let _mq = null;
function deviceMode() {
try { _mq = _mq || window.matchMedia('(prefers-color-scheme: dark)'); return _mq.matches ? 'dark' : 'light'; } catch (e) { return 'dark'; }
}
function modeOf(id, vars) { return MODES[id] || ((vars && vars['--color-scheme']) === 'light' ? 'light' : 'dark'); }
function otherHalf(id, mode) { return PAIRS[id] && MODES[PAIRS[id]] === mode ? PAIRS[id] : (mode === 'light' ? PAIRS[DEFAULT_ID] : DEFAULT_ID); }
function halfFor(themeId, vars, custom) {
if (readDisplay().scheme !== 'auto') return { id: themeId, vars: vars, custom: custom };
const want = deviceMode();
if (modeOf(themeId, vars) === want) return { id: themeId, vars: vars, custom: custom };
const other = otherHalf(themeId, want);
return { id: other, vars: THEMES[other], custom: false };
}
// The default theme in either mode: Vibe, and its light half. A season stands in for both, in the same mode.
function isDefault(id) { return id === DEFAULT_ID || id === PAIRS[DEFAULT_ID]; }
function show(themeId, vars, custom) {
_shown = { id: themeId, vars: vars, custom: !!custom };
// A theme being tried (?ov-try=) wins every repaint; _shown keeps the real one for Stop.
if (_trying && _trying.vars) return paintTry();
const half = halfFor(themeId, vars, !!custom);
const s = isDefault(half.id) && !half.custom && readDisplay().seasonal !== 'off' ? seasonNow() : null;
const seasonTheme = s ? (modeOf(half.id) === 'light' && s.light ? s.light : s.theme) : null;
const seasonal = seasonTheme && THEMES[seasonTheme] ? s : null;
const painted = seasonal ? seasonTheme : half.id;
applyVars(seasonal ? THEMES[seasonTheme] : half.vars);
const changed = (_season && _season.id) !== (seasonal && seasonal.id);
_season = seasonal;
try {
const el = document.documentElement;
if (seasonal) el.setAttribute('data-ov-season', seasonal.id); else el.removeAttribute('data-ov-season');
const marked = readDisplay().seasonal !== 'off' ? seasonNow() : null;
if (marked) el.setAttribute('data-ov-season-mark', marked.id); else el.removeAttribute('data-ov-season-mark');
el.setAttribute('data-theme', painted);
} catch { /* no DOM */ }
if (changed) { try { window.dispatchEvent(new CustomEvent('ov:season', { detail: seasonal })); } catch { /* */ } }
return painted;
}
/** The Light / Dark / Auto switch's state: 'auto', else the mode of the theme on screen. */
function getScheme() { return readDisplay().scheme === 'auto' ? 'auto' : modeOf(_shown.id, _shown.vars); }
/** Light or Dark saves the picked theme's other half (Auto off); Auto follows the device. → the new state. */
function setScheme(mode) {
if (mode === 'auto') { setDisplay({ scheme: 'auto' }); return 'auto'; }
if (mode !== 'light' && mode !== 'dark') return getScheme();
const cur = _shown;
if (readDisplay().scheme === 'auto') setDisplay({ scheme: 'manual' }, { noRepaint: true });
if (modeOf(cur.id, cur.vars) !== mode) save(otherHalf(cur.id, mode), null, { keepSeasonal: true });
else show(cur.id, cur.vars || THEMES[cur.id] || THEMES[DEFAULT_ID], cur.custom);
return mode;
}
function getCookie(name) {
const match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=([^;]*)'));
return match ? decodeURIComponent(match[1]) : null;
}
function setCookie(name, value, days) {
const maxAge = days * 24 * 60 * 60;
const encoded = encodeURIComponent(value);
const host = (typeof location !== 'undefined' && location.hostname) || '';
// Domain=.openvibe.tools only on openvibe.tools and its subdomains (CONTRACTS.md); host-only everywhere else.
if (host && (host.endsWith('.openvibe.tools') || host === 'openvibe.tools')) {
document.cookie = `${name}=${encoded};path=/;max-age=${maxAge};domain=.openvibe.tools;SameSite=Lax;Secure`;
} else {
const secure = (typeof location !== 'undefined' && location.protocol === 'https:') ? ';Secure' : '';
document.cookie = `${name}=${encoded};path=/;max-age=${maxAge};SameSite=Lax${secure}`;
}
}
/**
* Apply a theme's CSS variables to document.documentElement.
* If themeId is not found in the built-in map, does nothing.
*/
function applyById(themeId) {
const vars = THEMES[themeId];
if (!vars) return false;
applyVars(vars);
return true;
}
// Every token any built-in sets: applying a theme clears the ones it does not set (no inherited --success).
const KNOWN_PROPS = (function () { const out = {}; for (const id in THEMES) for (const k in THEMES[id]) out[k] = true; return Object.keys(out); })();
/** Readable text on a colour: near-black or white, whichever contrasts more (WCAG relative luminance). */
function onColor(c) {
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(c || '').trim());
if (!m) return null;
const h = m[1].length === 3 ? m[1].split('').map((x) => x + x).join('') : m[1];
const lin = (i) => { const v = parseInt(h.slice(i, i + 2), 16) / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
const L = 0.2126 * lin(0) + 0.7152 * lin(2) + 0.0722 * lin(4);
return (L + 0.05) / 0.05 >= 1.05 / (L + 0.05) ? '#0b0d10' : '#ffffff';
}
function applyVars(vars) {
if (!vars || typeof vars !== 'object') return;
// A theme without its own text-on-accent colours (custom and older server themes) gets readable ones derived.
if (vars['--accent'] && !vars['--on-accent']) {
const on = onColor(vars['--accent']);
if (on) vars = Object.assign({}, vars, { '--on-accent': on, '--on-accent-strong': vars['--on-accent-strong'] || onColor(vars['--accent-strong'] || vars['--accent']) || on });
}
const el = document.documentElement;
for (let i = 0; i < KNOWN_PROPS.length; i++) if (!(KNOWN_PROPS[i] in vars)) el.style.removeProperty(KNOWN_PROPS[i]);
for (const [prop, val] of Object.entries(vars)) {
if (prop.startsWith('--')) el.style.setProperty(prop, val);
}
// Native controls, scrollbars and form fields follow the theme's light/dark mode.
const scheme = vars['--color-scheme'];
if (scheme === 'light' || scheme === 'dark') { el.style.colorScheme = scheme; el.setAttribute('data-theme-mode', scheme); }
// The canvas follows the variables (value as fallback), so a page that sets --bg-primary later repaints it too.
if (vars['--bg-primary']) el.style.background = `var(--bg-primary, ${vars['--bg-primary']})`;
if (vars['--text-primary']) el.style.color = `var(--text-primary, ${vars['--text-primary']})`;
syncBrowserUi();
}
// ── A correction fades in ─────────────────────────────────
// A theme that changes under someone (the account's answer, another tab) crossfades for 0.4 s instead of jumping;
// first paint and their own picks stay instant. A colour fade moves nothing, so Calm keeps it; only Off stops it.
let _fadeTimer = 0;
function fadeNext() {
if (typeof document === 'undefined' || !document.head || motionOf() === 'off') return;
const el = document.documentElement;
if (!document.getElementById('ov-theme-fade-css')) {
const st = document.createElement('style'); st.id = 'ov-theme-fade-css';
st.textContent = 'html.ov-theme-fade,html.ov-theme-fade *,html.ov-theme-fade *::before,html.ov-theme-fade *::after{transition:background-color .4s ease,color .4s ease,border-color .4s ease,fill .4s ease,stroke .4s ease!important}';
document.head.appendChild(st);
}
el.classList.add('ov-theme-fade');
clearTimeout(_fadeTimer); _fadeTimer = setTimeout(function () { el.classList.remove('ov-theme-fade'); }, 600);
}
// ── The browser's UI follows the theme on every apply: theme-color, the favicon (),
// and window 'ov:theme' { accent, accentLight, bg, mode }.
// FAVICON:BEGIN — generated by scripts/build-theme-loader.js from mark.js and app-icon.js; do not edit by hand
const FAV_DEFS = '';
const FAV_BODY = '';
const FAV_LIGHT = { cx: 30.09, cy: 50, r: 5.58 };
const FAV_SITES = { network: ['#60a5fa', ''], live: ['#60a5fa', '#ef4444'], tools: ['#38bdf8', ''], community: ['#34d399', ''], games: ['#a78bfa', '#facc15'], media: ['#fbbf24', ''], space: ['#818cf8', ''], ai: ['#22d3ee', ''] };
// FAVICON:END
const COLOR_RE = /^(#[0-9a-f]{3,8}|rgba?\([\d\s.,%/]+\)|hsla?\([\d\s.,%/deg]+\))$/i;
function readColor(name, fallback) {
try { const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return COLOR_RE.test(v) ? v : fallback; } catch { return fallback; }
}
/** The tab icon: app-icon.js's favicon() (the Pair, heavy form, the site's glow and light) on the theme's ground. */
function faviconSvg(site, bg) {
const s = FAV_SITES[site] || FAV_SITES.network, a = s[0];
const light = s[1] ? '' : '';
return 'data:image/svg+xml,' + encodeURIComponent('');
}
let _browserUiKey = '';
function syncBrowserUi() {
if (typeof document === 'undefined' || !document.head) return;
const accent = readColor('--accent', '#3b82f6'), light = readColor('--accent-light', accent), bg = readColor('--bg-secondary', readColor('--bg-primary', '#0a0f1c'));
const mode = document.documentElement.getAttribute('data-theme-mode') || 'dark';
const key = [accent, light, bg, mode].join('|'); if (key === _browserUiKey) return; _browserUiKey = key;
try {
let meta = document.querySelector('meta[name="theme-color"]');
if (!meta) { meta = document.createElement('meta'); meta.name = 'theme-color'; document.head.appendChild(meta); }
meta.setAttribute('content', bg);
const link = document.querySelector('link[rel~="icon"][data-ov-icon]');
if (link) { link.type = 'image/svg+xml'; link.href = faviconSvg(link.getAttribute('data-ov-icon'), readColor('--bg-primary', '#0a0f1c')); }
} catch { /* cosmetic */ }
try { window.dispatchEvent(new CustomEvent('ov:theme', { detail: { accent: accent, accentLight: light, bg: bg, mode: mode } })); } catch { /* */ }
}
/** The theme from localStorage 'ov_theme' (id and variables), else the 'ov_theme_id' cookie, else the default. */
function resolveAndApply() {
// 1. Try localStorage (same-origin cache with full variables)
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const saved = JSON.parse(raw);
if (saved && saved.variables && typeof saved.variables === 'object') {
return show(saved.id || saved.slug || DEFAULT_ID, saved.variables, saved.custom);
}
if (saved && (saved.id || saved.slug)) {
const id = saved.id || saved.slug;
if (THEMES[id]) return show(id, THEMES[id]);
}
}
} catch { /* corrupt localStorage */ }
// 2. Try cross-domain cookie
const cookieId = getCookie(COOKIE_NAME);
if (cookieId && THEMES[cookieId]) {
// Cache the full variable set so the next load paints from localStorage instantly.
cacheLocally(cookieId, THEMES[cookieId]);
return show(cookieId, THEMES[cookieId]);
}
// 3. Default — vibe (already the CSS fallback values, but apply explicitly), or the season's theme
return show(DEFAULT_ID, THEMES[DEFAULT_ID]);
}
function authToken() {
return getCookie('ov_token') ||
(typeof localStorage !== 'undefined' && (localStorage.getItem('ov_token') || localStorage.getItem('token'))) ||
null;
}
/** Cache the theme's FULL variables (custom ones too) and the date it was chosen, so the next load paints at once. */
function cacheLocally(themeId, vars, custom, at) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
id: themeId, slug: themeId, variables: vars, at: at || Date.now(), ...(custom ? { custom: true } : {}),
}));
} catch { /* quota */ }
setCookie(COOKIE_NAME, themeId, 365);
}
/** Save a theme here and to the account (opts.custom: variable overrides; opts.sync === false: this device only). */
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;
// Picking the default theme while a season's stands in for it means "my normal theme": seasonal themes go off.
if (isDefault(themeId) && !custom && _season && !(opts && opts.keepSeasonal)) setDisplay({ seasonal: 'off' }, { noRepaint: true });
cacheLocally(themeId, vars, !!custom);
show(themeId, vars, !!custom); // saving a theme shows it: every token, the canvas and the browser UI follow at once
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', keepalive: true,
body: JSON.stringify({ theme_id: themeId, custom_variables: custom, at: themeAt() }),
}).catch(function () { /* offline / signed out — local cache still holds */ });
}
/** When the theme in this browser was chosen (ms), or 0 for a copy older than 4.64.0. */
function themeAt() {
try { const t = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null'); return t && Number(t.at) > 0 ? Number(t.at) : 0; } catch { return 0; }
}
/**
* Get the current theme ID (from cookie or localStorage).
*/
function getCurrent() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const saved = JSON.parse(raw);
if (saved && (saved.id || saved.slug)) return saved.id || saved.slug;
}
} catch {}
return getCookie(COOKIE_NAME) || 'vibe';
}
// ── Display settings, one record for every OpenVibe site: display.get() → DISPLAY_OPTS' values; display.set(patch)
// applies here and in other tabs and saves to the account. and --ov-text-scale before first paint.
const DISPLAY_KEY = 'ov_display', DISPLAY_OPTS = { motion: ['auto', 'full', 'reduced', 'off'], text: ['100', '112', '125'], seasonal: ['on', 'off'], scheme: ['manual', 'auto'], contrast: ['normal', 'more'], font: ['default', 'readable'], colors: ['default', 'cb-safe'], oled: ['off', 'on'], icons: ['auto', 'line', 'solid'], window: ['glass', 'solid', 'outline'] };
// Status names some sites use (--error, --error-text, --ok, --warn, --accent-text) that no theme sets: they resolve to the theme's
// readable status colours instead of each site's fallback literal (pale on light themes). :where() keeps them
// below any site's own value (4.86.0).
// A control nobody styled wears the theme, not the browser's grey (4.186.0): only one with no class, only its face,
// at no specificity, so any rule a site wrote still wins and no layout moves. Its shape is the kit's job.
const BARE = ':where(:not([class]))', FACE = '{font-family:inherit;border:1px solid var(--border-light,var(--border,color-mix(in srgb,currentColor 30%,transparent)));border-radius:10px;color:var(--text-primary,inherit);background:';
const BARE_CSS = ':where(button,input:is([type=submit],[type=button],[type=reset]))' + BARE + FACE + 'var(--bg-hover,var(--bg-card,color-mix(in srgb,currentColor 8%,transparent)))}'
+ ':where(input:not([type=checkbox],[type=radio],[type=range],[type=color],[type=file],[type=image],[type=submit],[type=button],[type=reset],[type=hidden]),select,textarea)' + BARE + FACE + 'var(--bg-input,var(--bg-primary,transparent))}'
+ ':where(input:is([type=checkbox],[type=radio],[type=range]),progress)' + BARE + '{accent-color:var(--accent,auto)}';
const ALIAS_CSS = ':where(html){--error:var(--danger);--error-text:var(--danger-text);--ok:var(--success-text);--warn:var(--warning-text);--accent-text:var(--accent-strong)}';
// Comfort (4.45.0): one attribute each, !important over the inline theme (Okabe-Ito colours, OLED black).
const COMFORT_CSS = 'html[data-ov-contrast=more]{--text-secondary:color-mix(in srgb,var(--text-primary) 88%,var(--bg-primary))!important;--text-muted:color-mix(in srgb,var(--text-primary) 76%,var(--bg-primary))!important;--border:color-mix(in srgb,var(--text-primary) 40%,var(--bg-primary))!important;--border-light:color-mix(in srgb,var(--text-primary) 55%,var(--bg-primary))!important}'
+ 'html[data-ov-contrast=more] :is(p,dd,td) a:not([class]){text-decoration-line:underline;text-underline-offset:2px}html[data-ov-contrast=more] :focus-visible{outline:3px solid var(--accent-strong,var(--accent))!important;outline-offset:2px}'
+ 'html[data-ov-font=readable]{--ov-font:"Atkinson Hyperlegible",Verdana,Tahoma,"Segoe UI",system-ui,sans-serif!important}html[data-ov-font=readable] body{font-family:var(--ov-font)!important;letter-spacing:.02em;word-spacing:.06em}html[data-ov-font=readable] :is(p,li,dd){line-height:1.7}'
+ 'html[data-ov-colors=cb-safe]{--success:#56b4e9!important;--success-text:#7cc6ef!important;--warning:#f0e442!important;--warning-text:#f0e442!important;--danger:#e8743b!important;--danger-text:#f0985f!important}'
+ 'html[data-ov-colors=cb-safe][data-theme-mode=light]{--success:#0072b2!important;--success-text:#005a8c!important;--warning:#e69f00!important;--warning-text:#8a6100!important;--danger:#d55e00!important;--danger-text:#a84400!important}'
+ 'html[data-ov-oled=on][data-theme-mode=dark]{--bg-primary:#000!important;--bg-secondary:#050608!important;--bg-tertiary:#0b0d12!important;--bg-card:#0a0c10!important;--bg-input:#0b0d12!important;--bg-hover:#12151c!important}';
const COMFORT_ATTRS = { contrast: ['data-ov-contrast', 'normal'], font: ['data-ov-font', 'default'], colors: ['data-ov-colors', 'default'], oled: ['data-ov-oled', 'off'], icons: ['data-ov-icons', 'auto'], window: ['data-ov-window', 'glass'] };
// ── Motion (4.183.0; the owner, 2026-11-10: "still have full motion but actually just reduce it instead of getting
// rid of it"). auto follows the device, full plays everything, reduced is Calm (every effect, smaller and slower),
// off alone stops motion. is the one signal; --ov-move is what a moving
// thing multiplies its distance by (display.move() in script); under Calm an endless loop runs at CALM_RATE.
const CALM_MOVE = 0.35, CALM_RATE = 0.6;
let _rm = null;
function deviceCalm() { try { _rm = _rm || window.matchMedia('(prefers-reduced-motion: reduce)'); return !!_rm.matches; } catch (e) { return false; } }
function motionOf(d) { const m = (d || readDisplay()).motion; return m === 'off' ? 'off' : m === 'reduced' ? 'calm' : m === 'full' ? 'full' : deviceCalm() ? 'calm' : 'full'; }
function moveOf() { const m = motionOf(); return m === 'off' ? 0 : m === 'calm' ? CALM_MOVE : 1; }
const MOTION_CSS = ':where(html){--ov-move:1}html[data-ov-motion=reduced]{--ov-move:' + CALM_MOVE + ';scroll-behavior:auto!important}html[data-ov-motion=off]{--ov-move:0}'
+ 'html[data-ov-motion=off] *:not([data-fx-demo] *),html[data-ov-motion=off] *:not([data-fx-demo] *)::before,html[data-ov-motion=off] *:not([data-fx-demo] *)::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}'
+ 'html[data-ov-motion=off]::view-transition-group(*),html[data-ov-motion=off]::view-transition-old(*),html[data-ov-motion=off]::view-transition-new(*){animation-duration:.001ms!important}'
+ 'html[data-ov-motion=off] .ov-icon .ovi-comet{display:none}';
const _paced = typeof WeakSet === 'function' ? new WeakSet() : null;
function paceLoops(scope) {
if (!_paced || typeof document === 'undefined') return;
try {
const calm = document.documentElement.getAttribute('data-ov-motion') === 'reduced';
const list = scope && scope.getAnimations ? scope.getAnimations({ subtree: true }) : document.getAnimations();
for (let i = 0; i < list.length; i++) {
const a = list[i], fx = a.effect, t = fx && fx.getComputedTiming ? fx.getComputedTiming() : null;
if (!t || t.iterations !== Infinity) continue;
const el = fx.target, slow = calm && !(el && el.closest && el.closest('[data-fx-demo]'));
if (slow && !_paced.has(a)) { _paced.add(a); a.updatePlaybackRate(a.playbackRate * CALM_RATE); }
else if (!slow && _paced.has(a)) { _paced.delete(a); a.updatePlaybackRate(a.playbackRate / CALM_RATE); }
}
} catch (e) { /* no Web Animations here: loops keep their own speed */ }
}
if (typeof document !== 'undefined' && document.addEventListener) {
document.addEventListener('animationstart', function (e) { if (document.documentElement.getAttribute('data-ov-motion') === 'reduced') paceLoops(e.target); }, true);
}
function readDisplay() {
const out = { motion: 'auto', text: '100', seasonal: 'on', scheme: 'manual', contrast: 'normal', font: 'default', colors: 'default', oled: 'off', icons: 'auto', window: 'glass' };
try { const d = JSON.parse(localStorage.getItem(DISPLAY_KEY) || 'null') || {}; for (const k in DISPLAY_OPTS) if (DISPLAY_OPTS[k].indexOf(String(d[k])) >= 0) out[k] = String(d[k]); } catch (e) { /* */ }
return out;
}
function applyDisplay(d) {
if (typeof document === 'undefined') return;
const el = document.documentElement;
const m = motionOf(d), attr = m === 'off' ? 'off' : m === 'calm' ? 'reduced' : d.motion === 'full' ? 'full' : '';
if (attr) el.setAttribute('data-ov-motion', attr); else el.removeAttribute('data-ov-motion');
if (d.text === '100') { el.style.removeProperty('--ov-text-scale'); el.removeAttribute('data-ov-text'); } else { el.style.setProperty('--ov-text-scale', String(parseInt(d.text, 10) / 100)); el.setAttribute('data-ov-text', d.text); }
for (const k in COMFORT_ATTRS) { const [attr, off] = COMFORT_ATTRS[k]; if (d[k] && d[k] !== off) el.setAttribute(attr, d[k]); else el.removeAttribute(attr); }
if (!document.getElementById('ov-display-css') && document.head) {
const st = document.createElement('style'); st.id = 'ov-display-css';
st.textContent = 'html[data-ov-text]{font-size:calc(100% * var(--ov-text-scale,1))}' + MOTION_CSS + BARE_CSS + ALIAS_CSS + COMFORT_CSS;
document.head.appendChild(st);
}
paceLoops();
try { window.dispatchEvent(new CustomEvent('ov:display', { detail: d })); } catch (e) { /* */ }
}
/** When the display settings in this browser were last changed (ms), or 0 for a copy older than 4.64.0. */
function displayAt() {
try { const d = JSON.parse(localStorage.getItem(DISPLAY_KEY) || 'null'); return d && Number(d.at) > 0 ? Number(d.at) : 0; } catch (e) { return 0; }
}
function setDisplay(patch, opts) {
const d = readDisplay();
const was = d.seasonal + '|' + d.scheme;
for (const k in DISPLAY_OPTS) if (patch && DISPLAY_OPTS[k].indexOf(String(patch[k])) >= 0) d[k] = String(patch[k]);
// A change made here is dated now; one adopted from the account or another site keeps its own date.
const at = opts && Number(opts.at) > 0 ? Number(opts.at) : (opts && opts.local ? displayAt() : Date.now());
try { localStorage.setItem(DISPLAY_KEY, JSON.stringify(Object.assign({}, d, { at: at }))); } catch (e) { /* */ }
applyDisplay(d);
if (d.seasonal + '|' + d.scheme !== was && !(opts && opts.noRepaint)) show(_shown.id, _shown.vars || THEMES[_shown.id] || THEMES[DEFAULT_ID], _shown.custom);
if (!(opts && opts.local) && typeof fetch !== 'undefined') {
const token = authToken(); const headers = { 'Content-Type': 'application/json' }; if (token) headers.Authorization = 'Bearer ' + token;
if (token || /(?:^|; )ov_sso_hint=account/.test(document.cookie)) fetch(_config.apiBase + '/api/themes/me/display', { method: 'PUT', headers: headers, credentials: 'include', keepalive: true, body: JSON.stringify(Object.assign({}, d, { at: at })) }).catch(function () {});
}
return d;
}
// ── Try a theme (4.41.0): ?ov-try= paints it for this tab without saving (?ov-try=off ends it); a bar
// offers Use it (saves to the account) and Stop. Only a name is read from the URL, never colours.
const TRY_KEY = 'ov_try';
const TRY_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
const tryName = function (id) { return String(id).split('-').map(function (w) { return w ? w[0].toUpperCase() + w.slice(1) : w; }).join(' '); };
function paintTry() {
applyVars(_trying.vars);
try {
const el = document.documentElement;
el.setAttribute('data-theme', _trying.id);
el.setAttribute('data-ov-trying', _trying.id);
el.removeAttribute('data-ov-season');
} catch { /* no DOM */ }
syncBrowserUi();
return _trying.id;
}
function readTry() {
let id = '';
try {
const p = typeof location !== 'undefined' ? new URLSearchParams(location.search).get('ov-try') : null;
if (p === 'off') { sessionStorage.removeItem(TRY_KEY); return null; }
if (p && TRY_ID.test(p)) sessionStorage.setItem(TRY_KEY, p);
id = sessionStorage.getItem(TRY_KEY) || '';
} catch { id = ''; }
return TRY_ID.test(id) ? id : null;
}
function startTry(id, name) {
if (!id || !TRY_ID.test(id)) return;
try { sessionStorage.setItem(TRY_KEY, id); } catch { /* private mode: this page only */ }
const begin = function (vars, label) {
if (!vars || !Object.keys(vars).length) { stopTry(false); return; }
_trying = { id: id, name: label || tryName(id), vars: vars };
fadeNext();
paintTry();
whenReady(tryBar);
};
if (THEMES[id]) return begin(THEMES[id], name);
if (typeof fetch === 'undefined') return;
// A gallery theme comes from the catalog, once the page has run init({ apiBase }) (the script tag after this one).
whenReady(function () {
fetch(_config.apiBase + '/api/themes/' + encodeURIComponent(id))
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) { begin(d && d.theme && d.theme.variables, d && d.theme && d.theme.name); })
.catch(function () { stopTry(false); });
});
}
function stopTry(repaint) {
_trying = null;
try { sessionStorage.removeItem(TRY_KEY); } catch { /* */ }
try {
document.documentElement.removeAttribute('data-ov-trying');
const bar = document.getElementById('ov-try-bar');
if (bar) bar.remove();
const u = new URL(location.href);
if (u.searchParams.has('ov-try')) { u.searchParams.delete('ov-try'); history.replaceState(history.state, '', u.pathname + (u.search || '') + u.hash); }
} catch { /* no DOM */ }
if (repaint !== false) { fadeNext(); resolveAndApply(); }
}
function whenReady(fn) {
if (typeof document === 'undefined') return;
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn, { once: true }); else fn();
}
function tryBar() {
if (!_trying || document.getElementById('ov-try-bar') || !document.body) return;
const t = _trying;
const bar = document.createElement('div');
bar.id = 'ov-try-bar';
bar.setAttribute('role', 'status');
bar.style.cssText = 'position:fixed;left:0;right:0;margin:0 auto;width:max-content;bottom:calc(var(--ovnav-appbar-h,0px) + 12px);z-index:2147483000;display:flex;align-items:center;gap:6px;max-width:calc(100vw - 24px);box-sizing:border-box;padding:4px 4px 4px 16px;border-radius:999px;border:1px solid var(--border-light,var(--border,#2c3d5c));background:var(--bg-card,#131c2e);color:var(--text-primary,#e6edf7);box-shadow:0 10px 30px rgba(0,0,0,.35);font:600 14px/1.3 var(--ov-font,system-ui,sans-serif)';
const label = document.createElement('span');
label.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0';
label.textContent = 'Trying ' + t.name;
const btn = function (text, strong) {
const b = document.createElement('button');
b.type = 'button';
b.textContent = text;
b.style.cssText = 'flex:none;min-height:44px;padding:0 16px;border-radius:999px;cursor:pointer;font:inherit;' + (strong
? 'border:0;background:var(--accent-strong,var(--accent,#3b82f6));color:var(--on-accent-strong,#fff)'
: 'border:1px solid var(--border-light,var(--border,#2c3d5c));background:transparent;color:inherit');
return b;
};
const use = btn('Use it', true);
const stop = btn('Stop', false);
use.addEventListener('click', function () {
stopTry(false);
save(t.id, t.vars);
const done = document.createElement('div');
done.setAttribute('role', 'status');
done.style.cssText = bar.style.cssText + ';padding:12px 18px';
done.textContent = t.name + ' is your theme now, on every OpenVibe site.';
document.body.appendChild(done);
setTimeout(function () { done.remove(); }, 3500);
});
stop.addEventListener('click', function () { stopTry(true); });
bar.append(label, use, stop);
document.body.appendChild(bar);
}
// ── The look travels (4.64.0): each domain keeps its own copy, so a click to another OpenVibe site carries ?ov_look=
// (what this page paints, the display and theme with their dates). Added at click time, read only after an OpenVibe
// page, only colours pass; the next site keeps it when newer and drops it from the address before first paint.
const OV_HOST = /(^|\.)(openvibe\.[a-z]{2,12}|openre\.stream)$/i;
const PAINT_KEY = 'ov_paint';
const PAINT_ATTRS = ['data-theme', 'data-theme-mode', 'data-ov-season', 'data-ov-season-mark', 'data-ov-motion', 'data-ov-text', 'data-ov-contrast', 'data-ov-font', 'data-ov-colors', 'data-ov-oled', 'data-ov-icons', 'data-ov-window'];
const SAFE_VALUE = /^(#[0-9a-f]{3,8}|rgba?\([0-9.,\s%]+\)|[0-9.,\s]+|light|dark|(-?[0-9.]+(px)?\s+){2,4}rgba?\([0-9.,\s%]+\))$/i;
const SAFE_ATTR = /^[a-z0-9-]{1,40}$/;
function seasonUntil(s) {
if (!s) return 0;
const d = new Date(), y = d.getFullYear(), to = s.to.split('-');
const end = new Date(y + (s.from > s.to && s.from <= String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') ? 1 : 0), Number(to[0]) - 1, Number(to[1]) + 1);
return end.getTime();
}
function paintRecord() {
const el = document.documentElement, a = {};
for (let i = 0; i < PAINT_ATTRS.length; i++) { const v = el.getAttribute(PAINT_ATTRS[i]); if (v) a[PAINT_ATTRS[i]] = v; }
return { k: THEME_KEYS.map(function (k) { return el.style.getPropertyValue(k).trim(); }), a: a, ts: el.style.getPropertyValue('--ov-text-scale').trim(), u: seasonUntil(_season), at: Date.now() };
}
let _paintTimer = 0;
function rememberPaint() {
if (_paintTimer || typeof setTimeout === 'undefined') return;
_paintTimer = setTimeout(function () { _paintTimer = 0; if (_trying) return; try { localStorage.setItem(PAINT_KEY, JSON.stringify(paintRecord())); } catch (e) { /* */ } }, 0);
}
function lookToken() {
const rec = { v: 1, d: readDisplay(), da: displayAt(), t: _shown.id, c: _shown.custom ? 1 : 0, ta: themeAt(), p: paintRecord() };
return btoa(JSON.stringify(rec)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function readLook() {
try {
if (typeof location === 'undefined' || !/[?&]ov_look=/.test(location.search)) return null;
const u = new URL(location.href), raw = u.searchParams.get('ov_look') || '';
u.searchParams.delete('ov_look');
try { history.replaceState(history.state, '', u.pathname + (u.search || '') + u.hash); } catch (e) { /* */ }
const from = document.referrer ? new URL(document.referrer).hostname : '';
if (!OV_HOST.test(from) || raw.length > 4000) return null;
const L = JSON.parse(atob(raw.replace(/-/g, '+').replace(/_/g, '/')));
return L && L.v === 1 ? L : null;
} catch (e) { return null; }
}
function adoptLook(L) {
if (!L) return;
const now = Date.now(), clamp = function (n) { n = Number(n) || 0; return n > now + 60000 ? now : n; };
const da = clamp(L.da), ta = clamp(L.ta);
if (L.d && typeof L.d === 'object' && da > displayAt()) {
const d = readDisplay();
for (const k in DISPLAY_OPTS) if (DISPLAY_OPTS[k].indexOf(String(L.d[k])) >= 0) d[k] = String(L.d[k]);
try { localStorage.setItem(DISPLAY_KEY, JSON.stringify(Object.assign({}, d, { at: da }))); } catch (e) { /* */ }
}
const id = String(L.t || '');
if (TRY_ID.test(id) && ta > themeAt()) {
if (!L.c && THEMES[id]) cacheLocally(id, THEMES[id], false, ta);
else if (L.c && L.p && Array.isArray(L.p.k)) {
const vars = {};
for (let i = 0; i < THEME_KEYS.length; i++) { const v = String(L.p.k[i] || ''); if (v && SAFE_VALUE.test(v)) vars[THEME_KEYS[i]] = v; }
if (Object.keys(vars).length > 8) cacheLocally(id, vars, true, ta);
}
}
}
function decorateLink(e) {
const a = e.target && e.target.closest && e.target.closest('a[href]');
if (!a || a.hasAttribute('download')) return;
let u; try { u = new URL(a.href, location.href); } catch (err) { return; }
if (!/^https?:$/.test(u.protocol) || u.hostname === location.hostname || !OV_HOST.test(u.hostname)) return;
try { u.searchParams.set('ov_look', lookToken()); a.href = u.toString(); } catch (err) { /* */ }
}
const _arrived = readLook();
adoptLook(_arrived);
applyDisplay(readDisplay());
if (typeof document !== 'undefined' && document.addEventListener) {
['pointerdown', 'keydown', 'click'].forEach(function (ev) { document.addEventListener(ev, function (e) { if (ev !== 'keydown' || e.key === 'Enter') decorateLink(e); }, true); });
window.addEventListener('ov:theme', rememberPaint);
window.addEventListener('ov:display', rememberPaint);
}
const activeId = resolveAndApply();
rememberPaint();
startTry(readTry());
// The icon link may be parsed after this script: repaint once the head is complete.
if (typeof document !== 'undefined' && document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { _browserUiKey = ''; syncBrowserUi(); });
// A theme picked in another tab (or on another OpenVibe page of this site) applies here at once.
if (typeof window !== 'undefined') window.addEventListener('storage', function (e) { if (e.key === 'ov_theme') { try { fadeNext(); resolveAndApply(); } catch (err) { /* */ } } if (e.key === DISPLAY_KEY) { fadeNext(); applyDisplay(readDisplay()); show(_shown.id, _shown.vars || THEMES[_shown.id] || THEMES[DEFAULT_ID], _shown.custom); } });
if (typeof window !== 'undefined' && window.matchMedia) {
try {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const onDevice = function () { if (readDisplay().scheme === 'auto') { fadeNext(); show(_shown.id, _shown.vars || THEMES[_shown.id] || THEMES[DEFAULT_ID], _shown.custom); } };
if (mq.addEventListener) mq.addEventListener('change', onDevice); else if (mq.addListener) mq.addListener(onDevice);
} catch (e) { /* no media queries */ }
}
if (typeof window !== 'undefined' && window.matchMedia) {
try {
const rmq = window.matchMedia('(prefers-reduced-motion: reduce)');
const onMotion = function () { if (readDisplay().motion === 'auto') applyDisplay(readDisplay()); };
if (rmq.addEventListener) rmq.addEventListener('change', onMotion); else if (rmq.addListener) rmq.addListener(onMotion);
} catch (e) { /* no media queries */ }
}
// A theme not in the built-in map (community, custom) comes from the catalog, never falls back to the default.
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; });
}
// ── The account's look from openvibe.network, at once (the cached look is already painted; newest copy wins). No
// token gate: the ov_token cookie rides along (credentials: 'include'), for sites whose script cannot read it.
let _lastSync = 0;
/** This site's id from (openvibe-shared/shell writes it), or '' (4.43.0). */
function siteService() {
try { const s = document.documentElement.getAttribute('data-ov-service') || ''; return /^[a-z][a-z0-9-]{1,31}$/.test(s) ? s : ''; } catch { return ''; }
}
function syncFromServer() {
if (typeof fetch === 'undefined') return;
_lastSync = Date.now();
const token = authToken();
const headers = token ? { 'Authorization': 'Bearer ' + token } : {};
// A per-site look set in OpenVibe.Design ("Games in Neon Tokyo") answers for ?service= (4.43.0).
const site = siteService();
fetch(_config.apiBase + '/api/themes/me/active' + (site ? '?service=' + encodeURIComponent(site) : ''), { headers: headers, credentials: 'include' })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (data) {
if (data && data.display) {
const cur = readDisplay(); const next = {}; for (const k in DISPLAY_OPTS) next[k] = data.display[k] || cur[k];
const serverAt = Number(data.display_at) || 0, localAt = displayAt();
const takeServer = serverAt ? serverAt > localAt : !(localAt && Date.now() - localAt < 60000);
if (takeServer && JSON.stringify(next) !== JSON.stringify(cur)) { fadeNext(); setDisplay(data.display, { local: true, at: serverAt || localAt }); }
else if (serverAt && localAt > serverAt && JSON.stringify(next) !== JSON.stringify(cur)) setDisplay(cur, { at: localAt }); // newer here: the account catches up
}
if (!data || !data.theme_id) return;
const serverThemeAt = Number(data.theme_at) || 0, localThemeAt = themeAt();
if (serverThemeAt ? serverThemeAt < localThemeAt : (localThemeAt && Date.now() - localThemeAt < 60000)) return; // a newer pick here
const serverId = data.theme_id;
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;
cacheLocally(serverId, vars, !!custom, serverThemeAt || localThemeAt || 1); // cacheLocally, not save() — no write-back loop
if (_shown.id === serverId && !!_shown.custom === !!custom && JSON.stringify(_shown.vars) === JSON.stringify(vars)) return;
fadeNext();
show(serverId, vars, !!custom);
};
if (builtin) return settle(builtin);
fetchThemeVars(serverId).then(settle);
})
.catch(function () { /* offline / signed out — keep the cached theme */ });
}
syncFromServer();
// Asked again when the page comes back into view, and shortly after arriving from another site (a change saved there
// just before the link may land after this page first asked).
if (typeof document !== 'undefined' && document.addEventListener) {
const again = function () { if (Date.now() - _lastSync > 10000) syncFromServer(); };
document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'visible') again(); });
if (typeof window !== 'undefined' && window.addEventListener) window.addEventListener('pageshow', function (e) { if (e && e.persisted) again(); });
let from = '';
try { from = document.referrer ? new URL(document.referrer).hostname : ''; } catch (e) { from = ''; }
if (from && typeof location !== 'undefined' && from !== location.hostname) setTimeout(syncFromServer, 2500);
}
// ── Public API ───────────────────────────────────────────
const OpenVibeThemeLoader = {
THEMES: THEMES,
display: { get: readDisplay, set: setDisplay, options: DISPLAY_OPTS, motion: function () { return motionOf(); }, move: moveOf },
apply: applyById,
applyVars: applyVars,
// Seasons: SEASONS, now() → the season today (or null), showing() → the season whose theme is on screen (or null)
seasons: { SEASONS: SEASONS, now: seasonNow, showing: function () { return _season; } },
// Light / Dark / Auto: get() → 'light'|'dark'|'auto', set(mode); pairOf(id) → its other half; modeOf(id)
scheme: { get: getScheme, set: setScheme, device: deviceMode },
pairOf: function (id) { return PAIRS[id] || null; },
modeOf: function (id) { return MODES[id] || null; },
onColor: onColor,
save: save,
// Try a theme on this tab without saving it (?ov-try=): start(id), stop(), current() → the id being tried, or null.
try: { start: function (id) { startTry(String(id || '')); }, stop: function () { stopTry(true); }, current: function () { return _trying ? _trying.id : null; } },
getCurrent: getCurrent,
// This site's id (), the key of its theme exception on the account (4.43.0).
site: function () { return siteService(); },
resolveAndApply: resolveAndApply,
init: function (opts) {
if (opts && typeof opts.apiBase === 'string' && opts.apiBase.trim()) {
_config.apiBase = opts.apiBase.trim();
}
return OpenVibeThemeLoader;
},
};
if (typeof module !== 'undefined' && module.exports) module.exports = OpenVibeThemeLoader;
else root.OpenVibeThemeLoader = OpenVibeThemeLoader;
})(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this);