// 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('' + FAV_DEFS + '\n' + '' + FAV_BODY + light + ''); } 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);