openvibers/openvibe.tools Commits
Font Awesome out, the OpenVibe glyphs in (plan L54 K4) (#171)
Changes
@@@ -22,7 +22,7 @@"multer": "^1.4.5-lts.1","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0","pg": "^8.23.0","uuid": "^11.1.0"}@@@ -1330,9 +1330,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.56.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",++ "version": "4.73.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -20,7 +20,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/style.css"></head><body>@@@ -45,10 +44,10 @@<button class="tool-tab" data-tool="normalize"><i class="fa-solid fa-sliders"></i> Normalize</button><button class="tool-tab" data-tool="fade"><i class="fa-solid fa-volume-low"></i> Fade</button><button class="tool-tab" data-tool="bass"><i class="fa-solid fa-volume-high"></i> Bass</button>-- <button class="tool-tab" data-tool="equalizer"><i class="fa-solid fa-bars-staggered"></i> EQ</button>++ <button class="tool-tab" data-tool="equalizer"><i class="fa-solid fa-sliders"></i> EQ</button><button class="tool-tab" data-tool="voice"><i class="fa-solid fa-user-astronaut"></i> Voice FX</button><button class="tool-tab" data-tool="echo"><i class="fa-solid fa-tower-broadcast"></i> Echo</button>-- <button class="tool-tab" data-tool="reverb"><i class="fa-solid fa-church"></i> Reverb</button>++ <button class="tool-tab" data-tool="reverb"><i class="fa-solid fa-house-chimney"></i> Reverb</button><button class="tool-tab" data-tool="extract"><i class="fa-solid fa-music"></i> Extract</button><button class="tool-tab" data-tool="ringtone"><i class="fa-solid fa-bell"></i> Ringtone</button></nav>@@@ -303,7 +302,7 @@<i class="fa-solid fa-download"></i> Download</a><button class="btn btn-secondary" id="another-btn">-- <i class="fa-solid fa-arrow-rotate-left"></i> Process Another++ <i class="fa-solid fa-rotate-left"></i> Process Another</button></div></section>@@@ -332,7 +331,7 @@<span class="cross-info"><span class="cross-name">OpenVibe.Live</span><span class="cross-desc">Live streaming platform</span></span></a><a class="cross-card" href="https://openvibe.games">-- <span class="cross-icon"><i class="fa-solid fa-dragon"></i></span>++ <span class="cross-icon"><i class="fa-solid fa-gamepad"></i></span><span class="cross-info"><span class="cross-name">Scraplandia</span><span class="cross-desc">Multiplayer RPG</span></span></a><a class="cross-card" href="https://maps.openvibe.tools">@@@ -648,7 +648,7 @@{ label: 'Format Conversion', icon: 'fa-solid fa-arrows-rotate', ids: ['mp3', 'wav', 'flac', 'ogg', 'm4a', 'aac', 'opus', 'wma', 'aiff', 'ac3'] },{ label: 'Audio Editing', icon: 'fa-solid fa-scissors', ids: ['trim', 'merge', 'fade', 'loop', 'silence', 'stereo', 'normalize', 'speed', 'reverse'] },{ label: 'Audio Effects', icon: 'fa-solid fa-wand-magic-sparkles', ids: ['pitch', 'bass', 'equalizer', 'echo', 'reverb', 'chorus', 'distortion', 'compressor', 'bitcrusher', 'voice', 'noise'] },-- { label: 'Extraction & Analysis', icon: 'fa-solid fa-magnifying-glass-chart', ids: ['extract', 'vocal', 'waveform', 'metadata'] },++ { label: 'Extraction & Analysis', icon: 'fa-solid fa-magnifying-glass', ids: ['extract', 'vocal', 'waveform', 'metadata'] },{ label: 'Specialized', icon: 'fa-solid fa-star', ids: ['ringtone', 'podcast'] },];@@@ -122,7 +122,7 @@ const DOMAIN_MAP = {},'loop.openvibe.tools': {toolId: 'loop', brandName: 'OpenVibeLoop', defaultOp: 'loop',-- faIcon: 'fa-repeat',++ faIcon: 'fa-arrows-rotate',seoTitle: 'OpenVibeLoop — Loop Audio Online',seoDescription: 'Loop audio files a set number of times. Create repeated versions of any sound. Online audio looper.',},@@@ -134,7 +134,7 @@ const DOMAIN_MAP = {},'equalizer.openvibe.tools': {toolId: 'equalizer', brandName: 'OpenVibeEQ', defaultOp: 'equalizer',-- faIcon: 'fa-bars-staggered',++ faIcon: 'fa-sliders',seoTitle: 'OpenVibeEQ — Online Audio Equalizer',seoDescription: 'Apply equalizer presets to audio files. Boost bass, treble, vocals and more. Online EQ tool.',},@@@ -174,7 +174,7 @@ const DOMAIN_MAP = {},'podcast.openvibe.tools': {toolId: 'podcast', brandName: 'OpenVibePodcast', defaultOp: 'podcast',-- faIcon: 'fa-podcast',++ faIcon: 'fa-microphone',seoTitle: 'OpenVibePodcast — Optimize Audio for Podcasts Online',seoDescription: 'Optimize audio for podcast publishing. Normalize loudness, compress dynamics, convert to podcast-ready format.',},@@@ -186,7 +186,7 @@ const DOMAIN_MAP = {},'noise.openvibe.tools': {toolId: 'noise', brandName: 'OpenVibeNoise', defaultOp: 'noise',-- faIcon: 'fa-broom',++ faIcon: 'fa-eraser',seoTitle: 'OpenVibeNoise — Reduce Background Noise Online',seoDescription: 'Remove background noise from audio recordings. Clean up interviews, podcasts, and voice memos. Online tool.',},@@@ -204,7 +204,7 @@ const DOMAIN_MAP = {},'reverb.openvibe.tools': {toolId: 'reverb', brandName: 'OpenVibeReverb', defaultOp: 'reverb',-- faIcon: 'fa-church',++ faIcon: 'fa-house-chimney',seoTitle: 'OpenVibeReverb — Add Reverb to Audio Online',seoDescription: 'Add room reverb, hall, cathedral, and plate reverb effects to audio. Online reverb tool.',},@@@ -95,9 +95,9 @@ app.use(helmet({contentSecurityPolicy: {directives: {defaultSrc: ["'self'"],-- scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://openvibe.network"],-- styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],-- fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],++ scriptSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://openvibe.network"],++ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],++ fontSrc: ["'self'", "https://fonts.gstatic.com"],imgSrc: ["'self'", "data:", "blob:", jobsRuntime.mediaOrigin()], // job result previews are 302s to MediamediaSrc: ["'self'", "blob:", jobsRuntime.mediaOrigin()],// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).@@@ -117,7 +117,7 @@ const TOOLS = {id: 'loop',label: 'Loop',description: 'Loop audio multiple times',-- faIcon: 'fa-repeat',++ faIcon: 'fa-arrows-rotate',accepts: AUDIO_FORMATS,category: 'editing',handler: loop,@@@ -135,7 +135,7 @@ const TOOLS = {id: 'equalizer',label: 'Equalizer',description: 'Apply EQ presets',-- faIcon: 'fa-bars-staggered',++ faIcon: 'fa-sliders',accepts: AUDIO_FORMATS,category: 'effects',handler: equalizer,@@@ -180,7 +180,7 @@ const TOOLS = {id: 'podcast',label: 'Podcast',description: 'Optimize for podcast publishing',-- faIcon: 'fa-podcast',++ faIcon: 'fa-microphone',accepts: AUDIO_FORMATS,category: 'specialized',handler: podcast,@@@ -198,7 +198,7 @@ const TOOLS = {id: 'noise',label: 'Noise Reduce',description: 'Reduce background noise',-- faIcon: 'fa-broom',++ faIcon: 'fa-eraser',accepts: AUDIO_FORMATS,category: 'editing',handler: noise,@@@ -225,7 +225,7 @@ const TOOLS = {id: 'reverb',label: 'Reverb',description: 'Add reverb effect',-- faIcon: 'fa-church',++ faIcon: 'fa-house-chimney',accepts: AUDIO_FORMATS,category: 'effects',handler: reverb,@@@ -21,7 +21,7 @@"multer": "^1.4.5-lts.1","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0","pdf-lib": "^1.17.1","pg": "^8.23.0","sharp": "^0.33.5",@@@ -1758,9 +1758,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.56.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",++ "version": "4.73.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -20,7 +20,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/style.css"></head><body>@@@ -40,8 +39,8 @@<button class="tool-tab" data-tool="split"><i class="fa-solid fa-scissors"></i> Split</button><button class="tool-tab" data-tool="compress"><i class="fa-solid fa-compress"></i> Compress</button><button class="tool-tab" data-tool="rotate"><i class="fa-solid fa-rotate"></i> Rotate</button>-- <button class="tool-tab" data-tool="reorder"><i class="fa-solid fa-sort"></i> Reorder</button>-- <button class="tool-tab" data-tool="watermark"><i class="fa-solid fa-stamp"></i> Watermark</button>++ <button class="tool-tab" data-tool="reorder"><i class="fa-solid fa-arrow-down-wide-short"></i> Reorder</button>++ <button class="tool-tab" data-tool="watermark"><i class="fa-solid fa-signature"></i> Watermark</button><button class="tool-tab" data-tool="img2pdf"><i class="fa-solid fa-file-image"></i> IMG→PDF</button><button class="tool-tab" data-tool="pdf2img"><i class="fa-solid fa-image"></i> PDF→IMG</button></nav>@@@ -208,7 +207,7 @@<i class="fa-solid fa-download"></i> Download</a><button class="btn btn-secondary" id="another-btn">-- <i class="fa-solid fa-arrow-rotate-left"></i> Process Another++ <i class="fa-solid fa-rotate-left"></i> Process Another</button></div></section>@@@ -671,10 +671,10 @@{ host: 'splitpdf.openvibe.tools', name: 'SplitPDF', icon: 'fa-scissors', desc: 'Split by page range' },{ host: 'compresspdf.openvibe.tools', name: 'CompressPDF', icon: 'fa-compress', desc: 'Reduce file size' },{ host: 'rotatepdf.openvibe.tools', name: 'RotatePDF', icon: 'fa-rotate', desc: 'Rotate PDF pages' },-- { host: 'reorderpdf.openvibe.tools', name: 'ReorderPDF', icon: 'fa-sort', desc: 'Rearrange pages' },-- { host: 'watermarkpdf.openvibe.tools', name: 'WatermarkPDF', icon: 'fa-stamp', desc: 'Add watermarks' },++ { host: 'reorderpdf.openvibe.tools', name: 'ReorderPDF', icon: 'fa-arrow-down-wide-short', desc: 'Rearrange pages' },++ { host: 'watermarkpdf.openvibe.tools', name: 'WatermarkPDF', icon: 'fa-signature', desc: 'Add watermarks' },{ host: 'protectpdf.openvibe.tools', name: 'ProtectPDF', icon: 'fa-lock', desc: 'Password protect' },-- { host: 'unlockpdf.openvibe.tools', name: 'UnlockPDF', icon: 'fa-lock-open', desc: 'Remove password' },++ { host: 'unlockpdf.openvibe.tools', name: 'UnlockPDF', icon: 'fa-unlock', desc: 'Remove password' },{ host: 'image2pdf.openvibe.tools', name: 'Image2PDF', icon: 'fa-file-image', desc: 'Images to PDF' },{ host: 'jpg2pdf.openvibe.tools', name: 'JPG2PDF', icon: 'fa-file-image', desc: 'JPG to PDF' },{ host: 'png2pdf.openvibe.tools', name: 'PNG2PDF', icon: 'fa-file-image', desc: 'PNG to PDF' },@@@ -50,13 +50,13 @@ const DOMAIN_MAP = {},'reorderpdf.openvibe.tools': {toolId: 'reorder', brandName: 'ReorderPDF', defaultOp: 'reorder',-- faIcon: 'fa-sort',++ faIcon: 'fa-arrow-down-wide-short',seoTitle: 'ReorderPDF — Rearrange PDF Pages Online',seoDescription: 'Drag and drop to reorder pages in your PDF. Remove unwanted pages. Online PDF page organizer.',},'watermarkpdf.openvibe.tools': {toolId: 'watermark', brandName: 'WatermarkPDF', defaultOp: 'watermark',-- faIcon: 'fa-stamp',++ faIcon: 'fa-signature',seoTitle: 'WatermarkPDF — Add Watermarks to PDFs Online',seoDescription: 'Add text or image watermarks to PDF documents. Customize position, opacity, rotation, and font. Online.',},@@@ -68,7 +68,7 @@ const DOMAIN_MAP = {},'unlockpdf.openvibe.tools': {toolId: 'unlock', brandName: 'UnlockPDF', defaultOp: 'unlock',-- faIcon: 'fa-lock-open',++ faIcon: 'fa-unlock',seoTitle: 'UnlockPDF — Remove PDF Password Online',seoDescription: 'Remove password protection from PDFs. Unlock PDF files for editing and printing. Online.',},@@@ -112,9 +112,9 @@ app.use(helmet({contentSecurityPolicy: {directives: {defaultSrc: ["'self'"],-- scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://openvibe.network"],-- styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],-- fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],++ scriptSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://openvibe.network"],++ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],++ fontSrc: ["'self'", "https://fonts.gstatic.com"],imgSrc: ["'self'", "data:", "blob:"],// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).connectSrc: ["'self'", "https://openvibe.network", "https://*.openvibe.tools", "https://openvibe.events"],@@@ -60,7 +60,7 @@ const TOOLS = {id: 'reorder',label: 'Reorder Pages',description: 'Rearrange or remove pages in a PDF',-- faIcon: 'fa-sort',++ faIcon: 'fa-arrow-down-wide-short',accepts: ['pdf'],multiFile: false,handler: reorder,@@@ -69,7 +69,7 @@ const TOOLS = {id: 'watermark',label: 'Watermark',description: 'Add text watermarks to PDF pages',-- faIcon: 'fa-stamp',++ faIcon: 'fa-signature',accepts: ['pdf'],multiFile: false,handler: watermark,@@@ -88,7 +88,7 @@ const TOOLS = {id: 'unlock',label: 'Unlock PDF',description: 'Remove password from a PDF',-- faIcon: 'fa-lock-open',++ faIcon: 'fa-unlock',accepts: ['pdf'],multiFile: false,requires: [qpdf], // decryption@@@ -17,7 +17,7 @@"helmet": "^7.1.0","iovalkey": "^0.4.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0","pg": "^8.23.0"}},@@@ -1129,9 +1129,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.56.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",++ "version": "4.73.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -32,7 +32,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"><script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.css">@@@ -45,7 +44,7 @@<div class="loc-search"><i class="fa-solid fa-location-dot"></i><input type="text" id="loc-input" placeholder="Enter your city or zip code...">-- <button id="btn-gps" title="Use GPS"><i class="fa-solid fa-crosshairs"></i></button>++ <button id="btn-gps" title="Use GPS"><i class="fa-solid fa-location-dot"></i></button><button id="btn-loc-search" class="btn-primary"><i class="fa-solid fa-magnifying-glass"></i> Find Food</button></div></div>@@@ -57,7 +56,7 @@<button class="tab active" data-tab="food-banks"><i class="fa-solid fa-hand-holding-heart"></i> Food Banks</button><button class="tab" data-tab="stores"><i class="fa-solid fa-cart-shopping"></i> Stores</button><button class="tab" data-tab="meal-plan"><i class="fa-solid fa-calendar-days"></i> Meal Planner</button>-- <button class="tab" data-tab="food-db"><i class="fa-solid fa-apple-whole"></i> Food Database</button>++ <button class="tab" data-tab="food-db"><i class="fa-solid fa-utensils"></i> Food Database</button><button class="tab" data-tab="map"><i class="fa-solid fa-map"></i> Map</button></nav>@@@ -117,7 +116,7 @@<input type="number" id="mp-days" value="3" min="1" max="7"></div><div class="control-group">-- <label><i class="fa-solid fa-toggle-on"></i> Options</label>++ <label><i class="fa-solid fa-circle-check"></i> Options</label><div class="toggle-row"><label class="toggle"><input type="checkbox" id="mp-camp" checked> <span>Camp-friendly</span></label><label class="toggle"><input type="checkbox" id="mp-shelf"> <span>Shelf-stable only</span></label>@@@ -131,7 +130,7 @@<!-- Food Database Panel --><section id="food-db" class="panel"><div class="panel-header">-- <h2><i class="fa-solid fa-apple-whole"></i> Budget Food Database</h2>++ <h2><i class="fa-solid fa-utensils"></i> Budget Food Database</h2><p class="panel-desc">Browse 80+ budget items with prices from 5 stores, nutrition info, and camp-friendliness ratings</p></div><div class="db-controls">@@@ -141,12 +140,12 @@</div><div class="db-filters"><button class="chip active" data-group="all">All</button>-- <button class="chip" data-group="protein"><i class="fa-solid fa-drumstick-bite"></i> Protein</button>-- <button class="chip" data-group="grains"><i class="fa-solid fa-wheat-awn"></i> Grains</button>-- <button class="chip" data-group="vegetables"><i class="fa-solid fa-carrot"></i> Veggies</button>-- <button class="chip" data-group="fruits"><i class="fa-solid fa-apple-whole"></i> Fruits</button>-- <button class="chip" data-group="dairy"><i class="fa-solid fa-glass-water"></i> Dairy</button>-- <button class="chip" data-group="fats"><i class="fa-solid fa-oil-can"></i> Fats</button>++ <button class="chip" data-group="protein"><i class="fa-solid fa-fish"></i> Protein</button>++ <button class="chip" data-group="grains"><i class="fa-solid fa-seedling"></i> Grains</button>++ <button class="chip" data-group="vegetables"><i class="fa-solid fa-leaf"></i> Veggies</button>++ <button class="chip" data-group="fruits"><i class="fa-solid fa-utensils"></i> Fruits</button>++ <button class="chip" data-group="dairy"><i class="fa-solid fa-mug-saucer"></i> Dairy</button>++ <button class="chip" data-group="fats"><i class="fa-solid fa-droplet"></i> Fats</button></div><div class="db-toggles"><label class="toggle"><input type="checkbox" id="db-camp"> <span>Camp-friendly</span></label>@@@ -71,7 +71,7 @@</div>${b.description ? `<div class="card-desc">${esc(b.description)}</div>` : ''}<div class="card-actions">-- ${b.lat && b.lon ? `<a class="card-btn" href="https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(b.lat)},${encodeURIComponent(b.lon)}" target="_blank"><i class="fa-solid fa-diamond-turn-right"></i> Directions</a>` : ''}++ ${b.lat && b.lon ? `<a class="card-btn" href="https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(b.lat)},${encodeURIComponent(b.lon)}" target="_blank"><i class="fa-solid fa-route"></i> Directions</a>` : ''}${site ? `<a class="card-btn" href="${esc(site)}" target="_blank" rel="noopener noreferrer"><i class="fa-solid fa-globe"></i> Website</a>` : ''}</div></div>`; }).join('');@@@ -101,7 +101,7 @@${s.phone ? `<span><i class="fa-solid fa-phone"></i> ${esc(s.phone)}</span>` : ''}</div><div class="card-actions">-- ${s.lat && s.lon ? `<a class="card-btn" href="https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(s.lat)},${encodeURIComponent(s.lon)}" target="_blank"><i class="fa-solid fa-diamond-turn-right"></i> Directions</a>` : ''}++ ${s.lat && s.lon ? `<a class="card-btn" href="https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(s.lat)},${encodeURIComponent(s.lon)}" target="_blank"><i class="fa-solid fa-route"></i> Directions</a>` : ''}</div></div>`;}).join('');@@@ -85,9 +85,9 @@ app.use(helmet({contentSecurityPolicy: {directives: {defaultSrc: ["'self'"],-- scriptSrc: ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net', 'unpkg.com', 'cdnjs.cloudflare.com', 'fonts.googleapis.com', 'https://openvibe.network'],-- styleSrc: ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net', 'unpkg.com', 'cdnjs.cloudflare.com', 'fonts.googleapis.com', 'fonts.gstatic.com'],-- fontSrc: ["'self'", 'fonts.gstatic.com', 'cdnjs.cloudflare.com'],++ scriptSrc: ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net', 'unpkg.com', 'fonts.googleapis.com', 'https://openvibe.network'],++ styleSrc: ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net', 'unpkg.com', 'fonts.googleapis.com', 'fonts.gstatic.com'],++ fontSrc: ["'self'", 'fonts.gstatic.com'],imgSrc: ["'self'", 'data:', 'image.tmdb.org', '*.tile.openstreetmap.org', '*.basemaps.cartocdn.com', 'server.arcgisonline.com'],// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).connectSrc: ["'self'", 'nominatim.openstreetmap.org', 'https://openvibe.network', 'https://openvibe.events'],@@@ -21,7 +21,7 @@"jsonwebtoken": "^9.0.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0","pg": "^8.23.0","zxing-wasm": "3.1.5"}@@@ -1229,9 +1229,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.56.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",++ "version": "4.73.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -25,7 +25,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><script src="/shared/theme-loader.js" defer></script><style>:root {@@@ -141,10 +140,10 @@const AUDIO_TOOLS = {'convert': { name: 'Converter', domain: 'convert.audio.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },'trim': { name: 'Trim', domain: 'trim.openvibe.tools', icon: 'fa-scissors', category: 'Edit' },-- 'merge': { name: 'Merge', domain: 'merge.openvibe.tools', icon: 'fa-code-merge', category: 'Mix' },++ 'merge': { name: 'Merge', domain: 'merge.openvibe.tools', icon: 'fa-code-fork', category: 'Mix' },'pitch': { name: 'Pitch', domain: 'pitch.openvibe.tools', icon: 'fa-wave-square', category: 'Effects' },'speed': { name: 'Speed', domain: 'speed.openvibe.tools', icon: 'fa-gauge-high', category: 'Edit' },-- 'reverb': { name: 'Reverb', domain: 'reverb.openvibe.tools', icon: 'fa-church', category: 'Effects' },++ 'reverb': { name: 'Reverb', domain: 'reverb.openvibe.tools', icon: 'fa-house-chimney', category: 'Effects' },'eq': { name: 'Equalizer', domain: 'equalizer.openvibe.tools', icon: 'fa-sliders', category: 'Effects' },'vocal': { name: 'Vocal Extract', domain: 'vocal.openvibe.tools', icon: 'fa-microphone-lines', category: 'Extract' },};@@@ -25,7 +25,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">--<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><style>:root{--bg:var(--bg-primary,#0b0c12);--bg2:var(--bg-secondary,#12131c);--bg3:var(--bg-tertiary,#1a1b28);--card:var(--bg-card,rgba(22,24,36,.85));--tx:var(--text-primary,#f1f4fb);--tx2:var(--text-secondary,#a0a8c4);--tx3:var(--text-muted,#5e6580);--ac:var(--accent,#a78bfa);--acl:var(--accent-light,#c4b5fd);--brd:var(--border,rgba(255,255,255,.07));--r:16px}*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}@@@ -178,12 +177,12 @@ html:{n:'HTML Formatter',i:'fa-file-code',c:'data',d:'HTML formatter, minifier &base64:{n:'Base64 Encoder & Decoder',i:'fa-lock',c:'enc',d:'Base64 encode & decode',p:'Hello, World!',m:['Encode','Decode']},url:{n:'URL Encoder & Decoder',i:'fa-link',c:'enc',d:'URL encode, decode & parser',p:'https://openvibe.tools/search?q=hello world&lang=en#results',m:['Encode','Decode','Parse']},jwt:{n:'JWT Decoder',i:'fa-key',c:'enc',d:'JWT decoder \u2014 header, payload & expiry',p:'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhvYm8iLCJpYXQiOjE1MTYyMzkwMjJ9.4S5J5Z5mSp5F8Kv3K5qFJpQ5Y5jF5p5t5m5k5e5h5d4',m:['Decode']},--uuid:{n:'UUID Generator',i:'fa-fingerprint',c:'enc',d:'UUID v4 generator & validator',p:'',m:['Generate','Bulk 10','Bulk 100','Validate']},++uuid:{n:'UUID Generator',i:'fa-id-card',c:'enc',d:'UUID v4 generator & validator',p:'',m:['Generate','Bulk 10','Bulk 100','Validate']},hash:{n:'Hash Generator',i:'fa-hashtag',c:'enc',d:'SHA-256, SHA-1 & SHA-512 hash generator',p:'Hello, World!',m:['SHA-256','SHA-1','SHA-512']},--hex:{n:'Hex Converter',i:'fa-barcode',c:'enc',d:'Hex encoder / decoder',p:'Hello',m:['To Hex','From Hex']},++hex:{n:'Hex Converter',i:'fa-code',c:'enc',d:'Hex encoder / decoder',p:'Hello',m:['To Hex','From Hex']},entities:{n:'String Escape Tool',i:'fa-shield-halved',c:'enc',d:'HTML, JS & URL escape / unescape',p:'<script>alert("hello")<\/script>',m:['HTML Enc','HTML Dec','JS Esc','JS Unesc','URL Enc','URL Dec']},timestamp:{n:'Unix Timestamp Converter',i:'fa-clock',c:'time',d:'Unix timestamp \u2194 date converter',p:'',m:['Now','To Date','To Unix']},--cron:{n:'Cron Expression Parser',i:'fa-calendar-check',c:'time',d:'Cron expression parser & next runs',p:'*/15 9-17 * * 1-5',m:['Parse']},++cron:{n:'Cron Expression Parser',i:'fa-calendar-days',c:'time',d:'Cron expression parser & next runs',p:'*/15 9-17 * * 1-5',m:['Parse']},beautify:{n:'Code Beautifier',i:'fa-wand-magic-sparkles',c:'qual',d:'Beautify JS, CSS, HTML & JSON',p:'{"a":1,"b":[2,3],"c":{"d":true}}',m:['JSON','HTML','CSS']},jsminify:{n:"JavaScript Minifier",i:'fa-compress',c:'qual',d:"Shrink JavaScript with a real parser, not regex",p:"function greet(name) {\n // say hello\n const message = \"Hello, \" + name + \"!\";\n console.log(message);\n return message;\n}\ngreet(\"OpenVibe\");",m:["Minify", "Minify + mangle", "Whitespace only"]},jsformat:{n:"JavaScript Beautifier",i:'fa-wand-magic-sparkles',c:'qual',d:"Turn minified or messy JavaScript into readable code",p:"function greet(n){const m=\"Hello, \"+n+\"!\";console.log(m);return m}greet(\"OpenVibe\");",m:["Beautify"]},@@@ -198,10 +197,10 @@ jsonstringify:{n:"JSON Stringify",i:'fa-quote-right',c:'data',d:"Turn JSON intohtml2md:{n:"HTML to Markdown",i:'fa-right-left',c:'data',d:"Convert HTML into clean Markdown",p:"\u003ch1>Hello World\u003c/h1>\n<p>This is <strong>bold</strong>, <em>italic</em> and a <a href=\"https://openvibe.network\">link</a>.</p>\n<ul><li>Item one</li><li>Item two</li></ul>\n<pre><code>code block</code></pre>",m:["Convert"]},md2html:{n:"Markdown to HTML",i:'fa-right-left',c:'data',d:"Convert Markdown into HTML you can paste anywhere",p:"# Hello World\n\nThis is **bold** and *italic* with a [link](https://openvibe.network).\n\n- Item one\n- Item two\n\n> A quote\n\n`inline code`",m:["To HTML", "Preview"]},minify:{n:'Code Minifier',i:'fa-compress',c:'qual',d:'Minify JS, CSS, HTML & JSON',p:'{\n "name": "openvibe",\n "version": 1\n}',m:['JSON','HTML','CSS']},--codediff:{n:'Diff Checker',i:'fa-code-compare',c:'qual',d:'Side-by-side text diff checker',p:'Line one\nLine two\nLine three\nLine four',p2:'Line one\nLine 2 modified\nLine three\nLine five\nLine six',dual:1,m:['Compare']},++codediff:{n:'Diff Checker',i:'fa-arrows-left-right',c:'qual',d:'Side-by-side text diff checker',p:'Line one\nLine two\nLine three\nLine four',p2:'Line one\nLine 2 modified\nLine three\nLine five\nLine six',dual:1,m:['Compare']},regex:{n:'Regex Tester',i:'fa-magnifying-glass',c:'qual',d:'Regex tester with match highlighting',p:'\\b[A-Z][a-z]+\\b',p2:'Hello World from OpenVibe.Tools today',dual:1,m:['Test']},slugify:{n:'Slug Generator',i:'fa-link',c:'qual',d:'URL slug generator',p:'Hello World! This is a Test -- 2024',m:['Generate']},--lorem:{n:'Lorem Ipsum Generator',i:'fa-paragraph',c:'qual',d:'Lorem ipsum generator',p:'',m:['1 Para','3 Para','5 Para','10 Sent','50 Words']},++lorem:{n:'Lorem Ipsum Generator',i:'fa-align-left',c:'qual',d:'Lorem ipsum generator',p:'',m:['1 Para','3 Para','5 Para','10 Sent','50 Words']},curlconvert:{n:'Online curl',i:'fa-terminal',c:'http',d:'Parse curl \u2192 fetch, Python, Node.js',p:"curl -X POST https://api.example.com/users -H 'Content-Type: application/json' -H 'Authorization: Bearer tok123' -d '{\"name\":\"openvibe\"}'",m:['To fetch','To Python','To Node','Parsed']},webhook:{n:'Webhook Tester',i:'fa-satellite-dish',c:'http',d:'Webhook request inspector bin',p:'',m:['Create Bin']},color:{n:'Color Converter',i:'fa-palette',c:'fe',d:'Color picker & HEX/RGB/HSL converter',p:'#a78bfa',m:['Convert']},@@@ -287,7 +286,7 @@ function showTool(){if(!framed){let ah='<button class="ab ab-go" onclick="run()"><i class="fa-solid fa-play"></i> Run</button>';ah+='<button class="ab ab-s" onclick="copyOut()"><i class="fa-solid fa-copy"></i> Copy</button>';-- ah+='<button class="ab ab-s" onclick="saveAsPaste(this)" title="Save the output as an unlisted paste on OpenVibe.Community"><i class="fa-solid fa-note-sticky"></i> Save as paste</button>';++ ah+='<button class="ab ab-s" onclick="saveAsPaste(this)" title="Save the output as an unlisted paste on OpenVibe.Community"><i class="fa-solid fa-paste"></i> Save as paste</button>';ah+='<button class="ab ab-s" onclick="clr()"><i class="fa-solid fa-trash"></i> Clear</button>';ah+='<button class="ab ab-s" onclick="sample()"><i class="fa-solid fa-flask"></i> Sample</button>';$('acts').innerHTML=ah;@@@ -25,7 +25,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><script src="/shared/theme-loader.js" defer></script><style>:root {@@@ -142,7 +141,7 @@'convert': { name: 'Converter', domain: 'convert.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },'compress': { name: 'Compress', domain: 'compress.openvibe.tools', icon: 'fa-compress', category: 'Optimize' },'resize': { name: 'Resize', domain: 'resize.openvibe.tools', icon: 'fa-expand', category: 'Resize' },-- 'crop': { name: 'Crop', domain: 'crop.openvibe.tools', icon: 'fa-crop', category: 'Edit' },++ 'crop': { name: 'Crop', domain: 'crop.openvibe.tools', icon: 'fa-scissors', category: 'Edit' },'png': { name: 'PNG', domain: 'png.openvibe.tools', icon: 'fa-image', category: 'Format' },'jpg': { name: 'JPG', domain: 'jpg.openvibe.tools', icon: 'fa-image', category: 'Format' },'webp': { name: 'WebP', domain: 'webp.openvibe.tools', icon: 'fa-image', category: 'Format' },@@@ -25,7 +25,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><style>:root {--bg-primary: #0b0c12;@@@ -671,21 +670,21 @@net: { name: 'Network Tools', icon: 'fa-network-wired', cat: 'hub', endpoint: null, placeholder: 'Enter a domain, IP, or URL…' },lookup: { name: 'Domain & IP Lookup', icon: 'fa-magnifying-glass', cat: 'hub', endpoint: '/lookup', placeholder: 'Domain, IP, or URL — we\'ll look up everything', btnText: 'Lookup All' },// IP / Identity-- myip: { name: 'What Is My IP', icon: 'fa-location-crosshairs', cat: 'ip', endpoint: '/myip', placeholder: null, auto: true },++ myip: { name: 'What Is My IP', icon: 'fa-location-dot', cat: 'ip', endpoint: '/myip', placeholder: null, auto: true },ip: { name: 'IP Address Lookup', icon: 'fa-at', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },ipv4: { name: 'IPv4 Lookup & CIDR Calculator', icon: 'fa-hashtag', cat: 'ip', endpoint: '/ipv4', placeholder: 'IPv4 address, CIDR block (e.g. 192.168.1.0/24) or domain' },ipv6: { name: 'IPv6 Lookup', icon: 'fa-code', cat: 'ip', endpoint: '/ipv6', placeholder: 'IPv6 address or domain' },geoip: { name: 'IP Geolocation', icon: 'fa-earth-americas', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },hostname: { name: 'Hostname Lookup', icon: 'fa-server', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address to resolve hostname' },-- isp: { name: 'ISP Lookup', icon: 'fa-building', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },++ isp: { name: 'ISP Lookup', icon: 'fa-city', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },asn: { name: 'ASN Lookup', icon: 'fa-diagram-project', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },rdns: { name: 'Reverse DNS Lookup', icon: 'fa-rotate-left', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address for reverse DNS' },-- whois: { name: 'Whois Lookup', icon: 'fa-address-book', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP' },++ whois: { name: 'Whois Lookup', icon: 'fa-address-card', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP' },rdap: { name: 'RDAP Lookup', icon: 'fa-id-card', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP for RDAP lookup' },// DNSdns: { name: 'DNS Lookup', icon: 'fa-sitemap', cat: 'dns', endpoint: '/dns', placeholder: 'Domain to query' },dig: { name: 'Online dig', icon: 'fa-terminal', cat: 'dns', endpoint: '/dns', placeholder: 'Domain (dig-style query)', dnsTypes: true },-- nslookup: { name: 'NSLookup', icon: 'fa-magnifying-glass-arrow-right', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for nslookup' },++ nslookup: { name: 'NSLookup', icon: 'fa-magnifying-glass', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for nslookup' },dnspropagation: { name: 'DNS Propagation Checker', icon: 'fa-globe', cat: 'dns', endpoint: '/dnsprop', placeholder: 'Domain to compare across public resolvers', propTypes: true },mx: { name: 'MX Record Lookup', icon: 'fa-envelope', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for MX records', defaultTypes: 'MX' },txt: { name: 'TXT Record Lookup', icon: 'fa-file-lines', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for TXT records', defaultTypes: 'TXT' },@@@ -699,7 +698,7 @@mtr: { name: 'MTR Report', icon: 'fa-chart-line', cat: 'diag', endpoint: '/mtr', placeholder: 'Host for an MTR report (10 probes a hop, from our server)', wait: 'about 15 seconds' },port: { name: 'Port Checker', icon: 'fa-door-open', cat: 'diag', endpoint: '/port', placeholder: 'Host to scan ports' },headers: { name: 'HTTP Header Checker', icon: 'fa-list', cat: 'diag', endpoint: '/headers', placeholder: 'URL or domain' },-- redirects: { name: 'Redirect Checker', icon: 'fa-share', cat: 'diag', endpoint: '/redirects', placeholder: 'URL to follow redirects' },++ redirects: { name: 'Redirect Checker', icon: 'fa-arrow-up-from-bracket', cat: 'diag', endpoint: '/redirects', placeholder: 'URL to follow redirects' },ssl: { name: 'SSL Certificate Checker', icon: 'fa-lock', cat: 'diag', endpoint: '/ssl', placeholder: 'Domain for SSL check' },curl: { name: 'Online curl', icon: 'fa-download', cat: 'diag', endpoint: '/headers', placeholder: 'URL' },httpstatus: { name: 'HTTP Status Codes', icon: 'fa-circle-check', cat: 'diag', endpoint: '/headers', placeholder: 'URL to check' },@@@ -715,7 +714,7 @@};const CATS = {-- ip: { label: 'IP / Identity / Ownership', icon: 'fa-fingerprint' },++ ip: { label: 'IP / Identity / Ownership', icon: 'fa-id-card' },dns: { label: 'DNS Tools', icon: 'fa-sitemap' },diag: { label: 'Active Diagnostics', icon: 'fa-satellite-dish' },mail: { label: 'Email & Reputation', icon: 'fa-envelope' },@@@ -31,22 +31,22 @@ const DEV_TOOLS = [{ id: 'base64', subdomain: 'base64', name: 'OpenVibeBase64', icon: 'fa-lock', desc: 'Base64 encode & decode text and files', category: 'encoding' },{ id: 'url', subdomain: 'url', name: 'OpenVibeURL', icon: 'fa-link', desc: 'URL encode, decode & component parser', category: 'encoding' },{ id: 'jwt', subdomain: 'jwt', name: 'OpenVibeJWT', icon: 'fa-key', desc: 'JWT decoder — inspect header, payload & expiry', category: 'encoding' },-- { id: 'uuid', subdomain: 'uuid', name: 'OpenVibeUUID', icon: 'fa-fingerprint', desc: 'UUID v4 generator, bulk generate & validator', category: 'encoding' },++ { id: 'uuid', subdomain: 'uuid', name: 'OpenVibeUUID', icon: 'fa-id-card', desc: 'UUID v4 generator, bulk generate & validator', category: 'encoding' },{ id: 'hash', subdomain: 'hash', name: 'OpenVibeHash', icon: 'fa-hashtag', desc: 'SHA-1, SHA-256 & SHA-512 hash generator', category: 'encoding' },-- { id: 'hex', subdomain: 'hex', name: 'OpenVibeHex', icon: 'fa-barcode', desc: 'Hex encoder/decoder & binary converter', category: 'encoding' },++ { id: 'hex', subdomain: 'hex', name: 'OpenVibeHex', icon: 'fa-code', desc: 'Hex encoder/decoder & binary converter', category: 'encoding' },{ id: 'escape', subdomain: 'entities', name: 'OpenVibeEscape', icon: 'fa-shield-halved', desc: 'HTML, JavaScript & URL escape/unescape', category: 'encoding' },// Time & Scheduling{ id: 'timestamp', subdomain: 'timestamp', name: 'OpenVibeTimestamp', icon: 'fa-clock', desc: 'Unix timestamp ↔ date converter with timezones', category: 'time' },-- { id: 'cron', subdomain: 'cron', name: 'OpenVibeCron', icon: 'fa-calendar-check', desc: 'Cron expression parser with next run calculator', category: 'time' },++ { id: 'cron', subdomain: 'cron', name: 'OpenVibeCron', icon: 'fa-calendar-days', desc: 'Cron expression parser with next run calculator', category: 'time' },// Code Quality{ id: 'beautify', subdomain: 'beautify', name: 'OpenVibeBeautify', icon: 'fa-wand-magic-sparkles', desc: 'Beautify & format JS, HTML, CSS, JSON & SQL', category: 'quality' },{ id: 'minify', subdomain: 'minify', name: 'OpenVibeMinify', icon: 'fa-compress', desc: 'Minify JS, HTML, CSS & JSON', category: 'quality' },-- { id: 'diff', subdomain: 'codediff', name: 'OpenVibeDiff', icon: 'fa-code-compare', desc: 'Side-by-side text & code diff checker', category: 'quality' },++ { id: 'diff', subdomain: 'codediff', name: 'OpenVibeDiff', icon: 'fa-arrows-left-right', desc: 'Side-by-side text & code diff checker', category: 'quality' },{ id: 'regex', subdomain: 'regex', name: 'OpenVibeRegex', icon: 'fa-magnifying-glass', desc: 'Regex tester with match highlighting & capture groups', category: 'quality' },{ id: 'slug', subdomain: 'slugify', name: 'OpenVibeSlug', icon: 'fa-link', desc: 'URL slug generator from any text', category: 'quality' },-- { id: 'lorem', subdomain: 'lorem', name: 'OpenVibeLorem', icon: 'fa-paragraph', desc: 'Lorem ipsum & placeholder text generator', category: 'quality' },++ { id: 'lorem', subdomain: 'lorem', name: 'OpenVibeLorem', icon: 'fa-align-left', desc: 'Lorem ipsum & placeholder text generator', category: 'quality' },// HTTP & API{ id: 'curl', subdomain: 'curlconvert', name: 'OpenVibeCurl', icon: 'fa-terminal', desc: 'Parse curl commands & convert to fetch, Python, Node.js', category: 'http' },@@@ -130,9 +130,9 @@ app.use(helmet({directives: {defaultSrc: ["'self'"],// 'wasm-unsafe-eval': the barcode scanner's ZXing reader is WebAssembly (codes.html), served from /vendor here.-- scriptSrc: ["'self'", "'unsafe-inline'", "'wasm-unsafe-eval'", "https://openvibe.network", "https://openvibe.live", "cdnjs.cloudflare.com", "cdn.jsdelivr.net", "fonts.googleapis.com", "https://static.cloudflareinsights.com"],-- styleSrc: ["'self'", "'unsafe-inline'", "cdnjs.cloudflare.com", "fonts.googleapis.com", "fonts.gstatic.com"],-- fontSrc: ["'self'", "fonts.gstatic.com", "cdnjs.cloudflare.com"],++ scriptSrc: ["'self'", "'unsafe-inline'", "'wasm-unsafe-eval'", "https://openvibe.network", "https://openvibe.live", "cdn.jsdelivr.net", "fonts.googleapis.com", "https://static.cloudflareinsights.com"],++ styleSrc: ["'self'", "'unsafe-inline'", "fonts.googleapis.com", "fonts.gstatic.com"],++ fontSrc: ["'self'", "fonts.gstatic.com"],imgSrc: ["'self'", "data:", "blob:", "https://openvibe.network", "https://openvibe.media"],// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).connectSrc: ["'self'", "https://cloudflareinsights.com", "https://openvibe.network", "https://openvibe.tools", "https://*.openvibe.tools", "https://openvibe.media", "https://openvibe.live", "https://openvibe.games", "https://openvibe.events"],@@@ -94,21 +94,21 @@ const NET_TOOLS = [{ id: 'net', subdomain: 'net', name: 'Net.OpenVibe', icon: 'fa-network-wired', desc: 'Network & Internet Diagnostics Hub', hub: true },{ id: 'lookup', subdomain: 'lookup', name: 'OpenVibeLookup', icon: 'fa-magnifying-glass', desc: 'All-in-one domain/IP lookup supertool', endpoint: '/lookup' },// IP / Identity-- { id: 'myip', subdomain: 'myip', name: 'OpenVibeMyIP', icon: 'fa-location-crosshairs', desc: 'Your public IP, ISP, location & reverse DNS', endpoint: '/myip' },++ { id: 'myip', subdomain: 'myip', name: 'OpenVibeMyIP', icon: 'fa-location-dot', desc: 'Your public IP, ISP, location & reverse DNS', endpoint: '/myip' },{ id: 'ip', subdomain: 'ip', name: 'OpenVibeIP', icon: 'fa-at', desc: 'IP address lookup & details (IPv4 & IPv6)', endpoint: '/ip' },{ id: 'ipv4', subdomain: 'ipv4', name: 'OpenVibeIPv4', icon: 'fa-hashtag', desc: 'IPv4 address lookup, class, CIDR calculator', endpoint: '/ipv4' },{ id: 'ipv6', subdomain: 'ipv6', name: 'OpenVibeIPv6', icon: 'fa-code', desc: 'IPv6 address lookup, expansion, type detection', endpoint: '/ipv6' },{ id: 'geoip', subdomain: 'geoip', name: 'OpenVibeGeoIP', icon: 'fa-earth-americas', desc: 'IP geolocation — country, city, timezone, map', endpoint: '/ip' },{ id: 'hostname', subdomain: 'hostname', name: 'OpenVibeHostname', icon: 'fa-server', desc: 'Hostname & reverse DNS lookup', endpoint: '/rdns' },-- { id: 'isp', subdomain: 'isp', name: 'OpenVibeISP', icon: 'fa-building', desc: 'ISP & organization info from IP', endpoint: '/ip' },++ { id: 'isp', subdomain: 'isp', name: 'OpenVibeISP', icon: 'fa-city', desc: 'ISP & organization info from IP', endpoint: '/ip' },{ id: 'asn', subdomain: 'asn', name: 'OpenVibeASN', icon: 'fa-diagram-project', desc: 'Autonomous System Number lookup', endpoint: '/ip' },{ id: 'rdns', subdomain: 'rdns', name: 'OpenVibeReverseDNS', icon: 'fa-rotate-left', desc: 'Reverse DNS (PTR) lookup for IPv4 & IPv6', endpoint: '/rdns' },-- { id: 'whois', subdomain: 'whois', name: 'OpenVibeWhois', icon: 'fa-address-book', desc: 'Whois domain registration lookup', endpoint: '/rdap' },++ { id: 'whois', subdomain: 'whois', name: 'OpenVibeWhois', icon: 'fa-address-card', desc: 'Whois domain registration lookup', endpoint: '/rdap' },{ id: 'rdap', subdomain: 'rdap', name: 'OpenVibeRDAP', icon: 'fa-id-card', desc: 'RDAP — modern registration data lookup', endpoint: '/rdap' },// DNS{ id: 'dns', subdomain: 'dns', name: 'OpenVibeDNS', icon: 'fa-sitemap', desc: 'DNS record lookup — A, AAAA, MX, TXT, and more', endpoint: '/dns' },{ id: 'dig', subdomain: 'dig', name: 'OpenVibeDig', icon: 'fa-terminal', desc: 'Online dig command — query any DNS record', endpoint: '/dns' },-- { id: 'nslookup', subdomain: 'nslookup', name: 'OpenVibeNSLookup', icon: 'fa-magnifying-glass-arrow-right', desc: 'NSLookup — simple DNS query tool', endpoint: '/dns' },++ { id: 'nslookup', subdomain: 'nslookup', name: 'OpenVibeNSLookup', icon: 'fa-magnifying-glass', desc: 'NSLookup — simple DNS query tool', endpoint: '/dns' },{ id: 'dnspropagation', subdomain: 'dnspropagation', name: 'OpenVibeDNSPropagation', icon: 'fa-globe', desc: 'Check DNS propagation across world regions', endpoint: '/dnsprop' },{ id: 'mx', subdomain: 'mx', name: 'OpenVibeMX', icon: 'fa-envelope', desc: 'MX record lookup — mail server discovery', endpoint: '/dns' },{ id: 'txt', subdomain: 'txt', name: 'OpenVibeTXT', icon: 'fa-file-lines', desc: 'TXT record lookup — SPF, DKIM, verification', endpoint: '/dns' },@@@ -122,7 +122,7 @@ const NET_TOOLS = [{ id: 'mtr', subdomain: 'mtr', name: 'OpenVibeMTR', icon: 'fa-chart-line', desc: 'MTR — combined ping + traceroute analysis', endpoint: '/mtr' },{ id: 'port', subdomain: 'port', name: 'OpenVibePortCheck', icon: 'fa-door-open', desc: 'Port scanner — check if ports are open', endpoint: '/port' },{ id: 'headers', subdomain: 'headers', name: 'OpenVibeHeaders', icon: 'fa-list', desc: 'HTTP headers checker — security & cache analysis', endpoint: '/headers' },-- { id: 'redirects', subdomain: 'redirects', name: 'OpenVibeRedirects', icon: 'fa-share', desc: 'Redirect chain tracer — follow all hops', endpoint: '/redirects' },++ { id: 'redirects', subdomain: 'redirects', name: 'OpenVibeRedirects', icon: 'fa-arrow-up-from-bracket', desc: 'Redirect chain tracer — follow all hops', endpoint: '/redirects' },{ id: 'ssl', subdomain: 'ssl', name: 'OpenVibeSSL', icon: 'fa-lock', desc: 'SSL/TLS certificate checker & chain inspector', endpoint: '/ssl' },{ id: 'curl', subdomain: 'curl', name: 'OpenVibeCurl', icon: 'fa-download', desc: 'Online curl — HTTP request tester', endpoint: '/headers' },{ id: 'httpstatus', subdomain: 'httpstatus', name: 'OpenVibeHTTPStatus', icon: 'fa-circle-check', desc: 'HTTP status code reference & checker', endpoint: '/headers' },@@@ -21,7 +21,7 @@"multer": "^1.4.5-lts.1","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0","pg": "^8.23.0","sharp": "^0.33.5","to-ico": "^1.1.5",@@@ -2264,9 +2264,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.56.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",++ "version": "4.73.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -20,7 +20,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/style.css"></head><body>@@@ -39,7 +38,7 @@<button class="tool-tab active" data-tool="convert"><i class="fa-solid fa-arrows-rotate"></i> Convert</button><button class="tool-tab" data-tool="compress"><i class="fa-solid fa-compress"></i> Compress</button><button class="tool-tab" data-tool="resize"><i class="fa-solid fa-up-right-and-down-left-from-center"></i> Resize</button>-- <button class="tool-tab" data-tool="crop"><i class="fa-solid fa-crop-simple"></i> Crop</button>++ <button class="tool-tab" data-tool="crop"><i class="fa-solid fa-scissors"></i> Crop</button></nav><!-- Upload Zone -->@@@ -152,7 +151,7 @@<i class="fa-solid fa-download"></i> Download</a><button class="btn btn-secondary" id="another-btn">-- <i class="fa-solid fa-arrow-rotate-left"></i> Process Another++ <i class="fa-solid fa-rotate-left"></i> Process Another</button></div></section>@@@ -519,14 +519,14 @@{ host: 'webp.openvibe.tools', name: 'OpenVibeWebP', icon: 'fa-file-image', desc: 'Convert to WebP' },{ host: 'avif.openvibe.tools', name: 'OpenVibeAVIF', icon: 'fa-file-image', desc: 'Convert to AVIF' },{ host: 'heic.openvibe.tools', name: 'OpenVibeHEIC', icon: 'fa-file-image', desc: 'HEIC Converter' },-- { host: 'svg.openvibe.tools', name: 'OpenVibeSVG', icon: 'fa-bezier-curve', desc: 'SVG Converter' },++ { host: 'svg.openvibe.tools', name: 'OpenVibeSVG', icon: 'fa-shapes', desc: 'SVG Converter' },{ host: 'gif.openvibe.tools', name: 'OpenVibeGIF', icon: 'fa-film', desc: 'GIF Converter' },{ host: 'ico.openvibe.tools', name: 'OpenVibeICO', icon: 'fa-icons', desc: 'ICO Favicon Maker' },{ host: 'tiff.openvibe.tools', name: 'OpenVibeTIFF', icon: 'fa-file-image', desc: 'Convert to TIFF' },{ host: 'bmp.openvibe.tools', name: 'OpenVibeBMP', icon: 'fa-file-image', desc: 'Convert to BMP' },{ host: 'compress.openvibe.tools', name: 'OpenVibeCompress', icon: 'fa-compress', desc: 'Compress Images' },{ host: 'resize.openvibe.tools', name: 'OpenVibeResize', icon: 'fa-up-right-and-down-left-from-center', desc: 'Resize Images' },-- { host: 'crop.openvibe.tools', name: 'OpenVibeCrop', icon: 'fa-crop-simple', desc: 'Crop Images' },++ { host: 'crop.openvibe.tools', name: 'OpenVibeCrop', icon: 'fa-scissors', desc: 'Crop Images' },{ host: 'convert.openvibe.tools', name: 'OpenVibeConvert', icon: 'fa-arrows-rotate', desc: 'Format Converter' },{ host: 'favicon.openvibe.tools', name: 'OpenVibeFavicon', icon: 'fa-icons', desc: 'Favicon Generator' },];@@@ -60,7 +60,7 @@ const DOMAIN_MAP = {},'svg.openvibe.tools': {toolId: 'svg', brandName: 'OpenVibeSVG', defaultOp: 'convert', defaultFormat: 'png',-- faIcon: 'fa-bezier-curve',++ faIcon: 'fa-shapes',seoTitle: 'OpenVibeSVG — Convert SVG Images Online',seoDescription: 'Convert SVG to PNG, JPG, WebP. Convert bitmap images to SVG vector traces. Online SVG converter.',},@@@ -104,7 +104,7 @@ const DOMAIN_MAP = {},'crop.openvibe.tools': {toolId: 'crop', brandName: 'OpenVibeCrop', defaultOp: 'crop',-- faIcon: 'fa-crop-simple',++ faIcon: 'fa-scissors',seoTitle: 'OpenVibeCrop — Crop Images Online',seoDescription: 'Crop images by custom dimensions or preset aspect ratios (16:9, 4:3, 1:1). Online image cropper.',},@@@ -101,9 +101,9 @@ app.use(helmet({contentSecurityPolicy: {directives: {defaultSrc: ["'self'"],-- scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://openvibe.network"],-- styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],-- fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],++ scriptSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://openvibe.network"],++ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],++ fontSrc: ["'self'", "https://fonts.gstatic.com"],imgSrc: ["'self'", "data:", "blob:", jobsRuntime.mediaOrigin()], // job result previews are 302s to Media// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).connectSrc: ["'self'", "https://openvibe.network", "https://*.openvibe.tools", "https://openvibe.events"],@@@ -43,7 +43,7 @@ const TOOLS = {id: 'crop',label: 'Crop',description: 'Crop images to specific areas or aspect ratios',-- faIcon: 'fa-crop-simple',++ faIcon: 'fa-scissors',accepts: ['png', 'jpg', 'jpeg', 'webp', 'avif', 'tiff', 'bmp', 'gif', 'heic', 'heif', 'svg'],outputs: null, // same as inputhandler: crop,@@@ -21,7 +21,7 @@"iovalkey": "^0.4.0","node-cache": "^5.1.2","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0","pg": "^8.23.0"}},@@@ -1446,9 +1446,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.56.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",++ "version": "4.73.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -35,8 +35,6 @@</script><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">-- <!-- Font Awesome 6 -->-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><!-- Leaflet --><link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css">@@@ -73,7 +71,7 @@</div><div class="splash-sources"><span><i class="fa-solid fa-database"></i> RIDB</span>-- <span><i class="fa-brands fa-openstreetmap"></i> OSM</span>++ <span><i class="fa-solid fa-map"></i> OSM</span><span><i class="fa-solid fa-campground"></i> FreeCampsites</span><span><i class="fa-solid fa-globe"></i> iOverlander</span><span><i class="fa-solid fa-bridge"></i> Bridges</span>@@@ -135,7 +133,7 @@<div class="search-box"><i class="fa-solid fa-magnifying-glass search-icon"></i><input type="text" id="search-input" placeholder="Address, city, or coordinates..." autocomplete="off">-- <button id="btn-my-location" title="Use GPS"><i class="fa-solid fa-crosshairs"></i></button>++ <button id="btn-my-location" title="Use GPS"><i class="fa-solid fa-location-dot"></i></button><select id="search-radius" title="Search radius"><option value="5">5 mi</option><option value="10">10 mi</option>@@@ -152,8 +150,8 @@<button id="btn-food" class="header-btn" title="Food Banks & Grocery"><i class="fa-solid fa-utensils"></i></button><button id="btn-favorites" class="header-btn" title="Favorites"><i class="fa-solid fa-heart"></i></button><button id="btn-add-spot" class="header-btn" title="Add Custom Spot"><i class="fa-solid fa-plus"></i></button>-- <button id="btn-guide" class="header-btn" title="Survival Guide"><i class="fa-solid fa-book-skull"></i></button>-- <button id="btn-stats" class="header-btn" title="Statistics"><i class="fa-solid fa-chart-pie"></i></button>++ <button id="btn-guide" class="header-btn" title="Survival Guide"><i class="fa-solid fa-book"></i></button>++ <button id="btn-stats" class="header-btn" title="Statistics"><i class="fa-solid fa-chart-column"></i></button><button id="btn-theme" class="header-btn" title="Toggle theme"><i class="fa-solid fa-moon"></i></button></div></header>@@@ -187,7 +185,7 @@</select><button id="btn-heatmap" class="filter-tool-btn" title="Stealth heatmap"><i class="fa-solid fa-fire"></i></button><button id="btn-crime-heatmap" class="filter-tool-btn" title="Crime/sketch heatmap"><i class="fa-solid fa-skull-crossbones"></i></button>-- <button id="btn-satellite" class="filter-tool-btn" title="Satellite view"><i class="fa-solid fa-satellite"></i></button>++ <button id="btn-satellite" class="filter-tool-btn" title="Satellite view"><i class="fa-solid fa-satellite-dish"></i></button></div></div>@@@ -214,7 +212,7 @@<div class="mini-stat"><i class="fa-solid fa-database"></i> 17+ Sources</div><div class="mini-stat"><i class="fa-solid fa-map-pin"></i> 100+ Spots</div><div class="mini-stat"><i class="fa-solid fa-cloud-sun"></i> Live Weather</div>-- <div class="mini-stat"><i class="fa-solid fa-globe-americas"></i> North America</div>++ <div class="mini-stat"><i class="fa-solid fa-earth-americas"></i> North America</div></div></div><div id="results-list"></div>@@@ -224,11 +222,11 @@<div id="map-container"><div id="map"></div><div class="map-ctrl-group">-- <button id="btn-map-locate" class="map-ctrl-btn" title="My location"><i class="fa-solid fa-street-view"></i></button>++ <button id="btn-map-locate" class="map-ctrl-btn" title="My location"><i class="fa-solid fa-map-location-dot"></i></button><button id="btn-map-fullscreen" class="map-ctrl-btn" title="Fullscreen"><i class="fa-solid fa-expand"></i></button></div><button id="btn-search-area" class="search-area-btn hidden">-- <i class="fa-solid fa-magnifying-glass-location"></i> Search This Area++ <i class="fa-solid fa-magnifying-glass"></i> Search This Area</button></div></main>@@@ -278,7 +276,7 @@<button class="food-tab active" data-tab="food-banks"><i class="fa-solid fa-hand-holding-heart"></i> Food Banks</button><button class="food-tab" data-tab="stores"><i class="fa-solid fa-cart-shopping"></i> Stores</button><button class="food-tab" data-tab="meal-plan"><i class="fa-solid fa-calendar-days"></i> Meal Plan</button>-- <button class="food-tab" data-tab="food-db"><i class="fa-solid fa-apple-whole"></i> Food Database</button>++ <button class="food-tab" data-tab="food-db"><i class="fa-solid fa-utensils"></i> Food Database</button></div><div id="food-banks" class="food-panel active"></div><div id="stores" class="food-panel hidden"></div>@@@ -353,15 +351,15 @@<div id="guide-modal" class="modal hidden"><div class="modal-content modal-lg"><div class="modal-header">-- <h2><i class="fa-solid fa-book-skull"></i> Survival Guide</h2>++ <h2><i class="fa-solid fa-book"></i> Survival Guide</h2><button class="modal-close"><i class="fa-solid fa-xmark"></i></button></div><div class="modal-body"><div class="guide-tabs"><button class="guide-tab active" data-guide="stealth"><i class="fa-solid fa-eye-slash"></i> Stealth</button><button class="guide-tab" data-guide="rules"><i class="fa-solid fa-gavel"></i> Rules</button>-- <button class="guide-tab" data-guide="gear"><i class="fa-solid fa-backpack"></i> Gear</button>-- <button class="guide-tab" data-guide="safety"><i class="fa-solid fa-shield-heart"></i> Safety</button>++ <button class="guide-tab" data-guide="gear"><i class="fa-solid fa-briefcase"></i> Gear</button>++ <button class="guide-tab" data-guide="safety"><i class="fa-solid fa-shield"></i> Safety</button><button class="guide-tab" data-guide="fire"><i class="fa-solid fa-fire"></i> Fire</button><button class="guide-tab" data-guide="firstaid"><i class="fa-solid fa-kit-medical"></i> First Aid</button></div>@@@ -374,7 +372,7 @@<div id="stats-modal" class="modal hidden"><div class="modal-content modal-md"><div class="modal-header">-- <h2><i class="fa-solid fa-chart-pie"></i> Statistics</h2>++ <h2><i class="fa-solid fa-chart-column"></i> Statistics</h2><button class="modal-close"><i class="fa-solid fa-xmark"></i></button></div><div class="modal-body">@@@ -74,7 +74,7 @@return 'default';}function typeIcon(tc) {-- return { dispersed:'fa-tree',campground:'fa-campground',forest:'fa-leaf',urban:'fa-city',services:'fa-hands-helping',++ return { dispersed:'fa-tree',campground:'fa-campground',forest:'fa-leaf',urban:'fa-city',services:'fa-handshake',water:'fa-faucet-drip',bridge:'fa-bridge',cover:'fa-umbrella',shelter:'fa-house-chimney',restroom:'fa-restroom',woods:'fa-tree',sketch:'fa-skull-crossbones',default:'fa-map-pin' }[tc]||'fa-map-pin';}@@@ -420,7 +420,7 @@toast(`Found ${state.locations.length} locations near ${query}`);} catch (err) {-- setStatus(`<i class="fa-solid fa-exclamation-triangle"></i> ${esc(err.message)}`);++ setStatus(`<i class="fa-solid fa-triangle-exclamation"></i> ${esc(err.message)}`);toast(err.message, 'error');$('#results-list').innerHTML = `<div class="muted-text"><i class="fa-solid fa-triangle-exclamation"></i> ${esc(err.message)}</div>`;} finally {@@@ -551,12 +551,12 @@${loc.amenities?.length?`<div class="detail-section"><h3><i class="fa-solid fa-list-check"></i> Amenities</h3><div class="result-tags">${loc.amenities.map(a=>`<span class="result-tag"><i class="fa-solid fa-check"></i> ${esc(a)}</span>`).join('')}</div></div>`:''}<div class="detail-section"><h3><i class="fa-solid fa-link"></i> Actions</h3><div class="detail-actions">-- <a class="detail-btn" href="https://www.google.com/maps/dir/?api=1&destination=${num(loc.lat)},${num(loc.lon)}" target="_blank"><i class="fa-solid fa-diamond-turn-right"></i> Directions</a>++ <a class="detail-btn" href="https://www.google.com/maps/dir/?api=1&destination=${num(loc.lat)},${num(loc.lon)}" target="_blank"><i class="fa-solid fa-route"></i> Directions</a><a class="detail-btn" href="https://www.google.com/maps/@${num(loc.lat)},${num(loc.lon)},15z" target="_blank"><i class="fa-solid fa-map"></i> Google Maps</a><button class="detail-btn" id="btn-copy-coords"><i class="fa-solid fa-copy"></i> Copy Coords</button></div></div>-- <div class="detail-section"><h3><i class="fa-solid fa-note-sticky"></i> Notes</h3>++ <div class="detail-section"><h3><i class="fa-solid fa-paste"></i> Notes</h3><div class="detail-note-area"><textarea id="detail-note" placeholder="Your notes...">${esc(noteText)}</textarea><button class="detail-btn primary" id="btn-save-note"><i class="fa-solid fa-floppy-disk"></i> Save</button>@@@ -618,7 +618,7 @@if (!w || !state.searchCenter) { c.innerHTML = '<p class="muted-text">Search for a location first.</p>'; show($('#weather-modal')); return; }let html = '<div class="detail-section">';if (w.current) {-- html += `<h3><i class="fa-solid fa-temperature-half"></i> Current Conditions</h3>`;++ html += `<h3><i class="fa-solid fa-sun"></i> Current Conditions</h3>`;if (w.current.temperature != null) html += `<div class="detail-info-row"><span class="label">Temperature</span><span class="value">${Math.round(w.current.temperature)}°F</span></div>`;if (w.current.description) html += `<div class="detail-info-row"><span class="label">Conditions</span><span class="value">${esc(w.current.description)}</span></div>`;if (w.current.windSpeed != null) html += `<div class="detail-info-row"><span class="label">Wind</span><span class="value">${w.current.windSpeed} mph</span></div>`;@@@ -855,7 +855,7 @@<li><strong>City/County Parks</strong> — Camping usually prohibited.</li><li><strong>Private Land</strong> — Need owner permission.</li><li><strong>Walmart/Rest Stops</strong> — Varies by location.</li></ul></div>`,-- gear: `<div class="detail-section"><h3><i class="fa-solid fa-backpack"></i> Gear Checklist</h3><ul>++ gear: `<div class="detail-section"><h3><i class="fa-solid fa-briefcase"></i> Gear Checklist</h3><ul><li><strong>Shelter:</strong> Dark bivy/tarp/tent</li><li><strong>Sleep:</strong> 20°F bag + insulated pad</li><li><strong>Light:</strong> Red headlamp</li>@@@ -866,7 +866,7 @@<li><strong>First Aid:</strong> Kit + emergency blanket + whistle</li><li><strong>Tools:</strong> Multi-tool, paracord 50ft, trash bags</li><li><strong>Comms:</strong> Charged phone + battery. PLB for remote</li></ul></div>`,-- safety: `<div class="detail-section"><h3><i class="fa-solid fa-shield-heart"></i> Safety</h3><ul>++ safety: `<div class="detail-section"><h3><i class="fa-solid fa-shield"></i> Safety</h3><ul><li><strong>Tell someone</strong> your plans</li><li><strong>Wildlife:</strong> Hang food 200ft from camp in bear country</li><li><strong>Rivers</strong> rise fast with rain</li>@@@ -78,12 +78,12 @@ app.use(helmet({contentSecurityPolicy: {directives: {defaultSrc: ["'self'"],-- scriptSrc: ["'self'", "'unsafe-inline'", "unpkg.com", "cdn.jsdelivr.net", "cdnjs.cloudflare.com", "static.cloudflareinsights.com", "https://openvibe.network"],-- styleSrc: ["'self'", "'unsafe-inline'", "unpkg.com", "cdn.jsdelivr.net", "cdnjs.cloudflare.com", "fonts.googleapis.com"],++ scriptSrc: ["'self'", "'unsafe-inline'", "unpkg.com", "cdn.jsdelivr.net", "static.cloudflareinsights.com", "https://openvibe.network"],++ styleSrc: ["'self'", "'unsafe-inline'", "unpkg.com", "cdn.jsdelivr.net", "fonts.googleapis.com"],imgSrc: ["'self'", "data:", "blob:", "*.tile.openstreetmap.org", "*.basemaps.cartocdn.com", "server.arcgisonline.com", "image.tmdb.org", "*.wp.com"],// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).connectSrc: ["'self'", "nominatim.openstreetmap.org", "api.weather.gov", "api.open-meteo.com", "*.tile.openstreetmap.org", "*.basemaps.cartocdn.com", "server.arcgisonline.com", "https://openvibe.network", "https://openvibe.events"],-- fontSrc: ["'self'", "fonts.gstatic.com", "cdnjs.cloudflare.com", "cdn.jsdelivr.net"],++ fontSrc: ["'self'", "fonts.gstatic.com", "cdn.jsdelivr.net"],frameSrc: ["'none'"],scriptSrcAttr: ["'unsafe-inline'"],},@@@ -199,8 +199,8 @@ function getSourceDefs(lat, lon, radius) {{ name: 'Woods', icon: 'fa-tree', fn: () => woods.findWoods(lat, lon, radius) },{ name: 'Waterways', icon: 'fa-water', fn: () => waterways.findWaterways(lat, lon, radius) },{ name: 'NPS', icon: 'fa-mountain-sun', fn: () => nps.search(lat, lon, radius, config.npsApiKey) },-- { name: 'OpenChargeMap', icon: 'fa-charging-station', fn: () => openchargemap.search(lat, lon, radius, config.openChargeMapKey) },-- { name: 'WebScraper', icon: 'fa-spider', fn: () => scraper.search(lat, lon, radius) },++ { name: 'OpenChargeMap', icon: 'fa-bolt', fn: () => openchargemap.search(lat, lon, radius, config.openChargeMapKey) },++ { name: 'WebScraper', icon: 'fa-bug', fn: () => scraper.search(lat, lon, radius) },{ name: 'Rain Cover', icon: 'fa-umbrella', fn: () => cover.findCover(lat, lon, radius) },{ name: 'Crime Intel', icon: 'fa-skull-crossbones', fn: () => crimedata.findSketchAreas(lat, lon, radius) },{ name: 'Harm Reduction', icon: 'fa-suitcase-medical', fn: () => harmreduction.findHarmReduction(lat, lon, radius) },@@@ -21,17 +21,17 @@ const BATHROOM_TYPES = {public: { icon: 'fa-restroom', label: 'Public Restroom', color: '#3b82f6' },park: { icon: 'fa-tree', label: 'Park Restroom', color: '#22c55e' },library: { icon: 'fa-book', label: 'Library', color: '#8b5cf6' },-- gas_station: { icon: 'fa-gas-pump', label: 'Gas Station', color: '#f59e0b' },++ gas_station: { icon: 'fa-store', label: 'Gas Station', color: '#f59e0b' },restaurant: { icon: 'fa-utensils', label: 'Restaurant/Café', color: '#ef4444' },mall: { icon: 'fa-store', label: 'Store/Mall', color: '#ec4899' },-- transit: { icon: 'fa-bus', label: 'Transit Station', color: '#06b6d4' },-- community: { icon: 'fa-building', label: 'Community Building', color: '#6366f1' },-- shower: { icon: 'fa-shower', label: 'Shower Facility', color: '#14b8a6' },-- gym_shower: { icon: 'fa-dumbbell', label: 'Gym Shower', color: '#8b5cf6' },++ transit: { icon: 'fa-train', label: 'Transit Station', color: '#06b6d4' },++ community: { icon: 'fa-city', label: 'Community Building', color: '#6366f1' },++ shower: { icon: 'fa-faucet', label: 'Shower Facility', color: '#14b8a6' },++ gym_shower: { icon: 'fa-weight-hanging', label: 'Gym Shower', color: '#8b5cf6' },pool_shower: { icon: 'fa-person-swimming', label: 'Pool/Sports Shower', color: '#0ea5e9' },hot_spring: { icon: 'fa-hot-tub-person', label: 'Hot Spring', color: '#f97316' },water: { icon: 'fa-faucet-drip', label: 'Drinking Water', color: '#0ea5e9' },-- unknown: { icon: 'fa-toilet', label: 'Restroom', color: '#94a3b8' },++ unknown: { icon: 'fa-restroom', label: 'Restroom', color: '#94a3b8' },};// ─── Classify bathroom type from tags/name ─────────────────────────@@@ -24,15 +24,15 @@ const { haversine, overpassQuery } = require('./utils');// ── Cover Type Definitions ──────────────────────────────────────────const COVER_TYPES = {covered_walkway: { label: 'Covered Walkway', icon: 'fa-road', color: '#6366f1', stealth: 2 },-- arcade: { label: 'Arcade / Colonnade', icon: 'fa-archway', color: '#8b5cf6', stealth: 2 },-- canopy: { label: 'Canopy / Awning', icon: 'fa-tent-arrow-down-to-line', color: '#f59e0b', stealth: 2 },-- carport: { label: 'Carport', icon: 'fa-car-side', color: '#64748b', stealth: 3 },++ arcade: { label: 'Arcade / Colonnade', icon: 'fa-door-open', color: '#8b5cf6', stealth: 2 },++ canopy: { label: 'Canopy / Awning', icon: 'fa-campground', color: '#f59e0b', stealth: 2 },++ carport: { label: 'Carport', icon: 'fa-square-parking', color: '#64748b', stealth: 3 },porch: { label: 'Building Porch', icon: 'fa-house-chimney', color: '#f97316', stealth: 2 },covered_market: { label: 'Covered Market', icon: 'fa-store', color: '#ec4899', stealth: 1 },-- covered_seating: { label: 'Covered Seating', icon: 'fa-chair', color: '#14b8a6', stealth: 1 },++ covered_seating: { label: 'Covered Seating', icon: 'fa-couch', color: '#14b8a6', stealth: 1 },covered_platform: { label: 'Covered Platform', icon: 'fa-train', color: '#06b6d4', stealth: 1 },grandstand: { label: 'Grandstand / Bleachers', icon: 'fa-flag', color: '#3b82f6', stealth: 3 },-- loading_dock: { label: 'Loading Dock / Bay', icon: 'fa-warehouse', color: '#94a3b8', stealth: 3 },++ loading_dock: { label: 'Loading Dock / Bay', icon: 'fa-box-open', color: '#94a3b8', stealth: 3 },covered_area: { label: 'Covered Area', icon: 'fa-cloud-rain', color: '#22c55e', stealth: 2 },curated: { label: 'Known Cover Spot', icon: 'fa-umbrella', color: '#a855f7', stealth: 3 },};@@@ -18,29 +18,29 @@ const { haversine, overpassQuery } = require('./utils');// Higher weight = stronger signal of "sketchy" areaconst GRIT_INDICATORS = {// Nightlife / vice-- bar: { query: '["amenity"="bar"]', weight: 2, label: 'Bar', icon: 'fa-martini-glass' },++ bar: { query: '["amenity"="bar"]', weight: 2, label: 'Bar', icon: 'fa-mug-hot' },nightclub: { query: '["amenity"="nightclub"]', weight: 3, label: 'Nightclub', icon: 'fa-music' },-- pub: { query: '["amenity"="pub"]', weight: 1, label: 'Pub', icon: 'fa-beer-mug-empty' },++ pub: { query: '["amenity"="pub"]', weight: 1, label: 'Pub', icon: 'fa-mug-saucer' },strip_club: { query: '["amenity"="stripclub"]', weight: 4, label: 'Strip Club', icon: 'fa-mask' },casino: { query: '["amenity"="casino"]', weight: 3, label: 'Casino', icon: 'fa-dice' },adult_shop: { query: '["shop"="erotic"]', weight: 3, label: 'Adult Shop', icon: 'fa-store' },// Commerce grit-- pawn_shop: { query: '["shop"="pawnbroker"]', weight: 3, label: 'Pawn Shop', icon: 'fa-ring' },++ pawn_shop: { query: '["shop"="pawnbroker"]', weight: 3, label: 'Pawn Shop', icon: 'fa-gem' },payday_loan: { query: '["shop"="money_lender"]', weight: 3, label: 'Payday Lender', icon: 'fa-money-bill' },-- liquor_store: { query: '["shop"="alcohol"]', weight: 2, label: 'Liquor Store', icon: 'fa-wine-bottle' },++ liquor_store: { query: '["shop"="alcohol"]', weight: 2, label: 'Liquor Store', icon: 'fa-flask' },tattoo_shop: { query: '["shop"="tattoo"]', weight: 1, label: 'Tattoo Parlor', icon: 'fa-pen-nib' },-- vape_shop: { query: '["shop"="e-cigarette"]', weight: 1, label: 'Vape/Smoke Shop', icon: 'fa-smoking' },-- tobacco_shop: { query: '["shop"="tobacco"]', weight: 1, label: 'Tobacco Shop', icon: 'fa-smoking' },++ vape_shop: { query: '["shop"="e-cigarette"]', weight: 1, label: 'Vape/Smoke Shop', icon: 'fa-smog' },++ tobacco_shop: { query: '["shop"="tobacco"]', weight: 1, label: 'Tobacco Shop', icon: 'fa-smog' },// Social services (indicates street population density)homeless_shelter:{ query: '["social_facility"="shelter"]["social_facility:for"~"homeless|underprivileged"]', weight: 2, label: 'Homeless Shelter', icon: 'fa-house-chimney' },food_bank: { query: '["social_facility"="food_bank"]', weight: 1, label: 'Food Bank', icon: 'fa-utensils' },// Law enforcement (indicates high-incident areas)-- police: { query: '["amenity"="police"]', weight: 1, label: 'Police Station', icon: 'fa-building-shield' },++ police: { query: '["amenity"="police"]', weight: 1, label: 'Police Station', icon: 'fa-shield-halved' },// Transportation (transient hubs)-- bus_station: { query: '["amenity"="bus_station"]', weight: 1, label: 'Bus Station', icon: 'fa-bus' },++ bus_station: { query: '["amenity"="bus_station"]', weight: 1, label: 'Bus Station', icon: 'fa-train' },// Abandoned / sketchy structures-- abandoned: { query: '["abandoned"="yes"]', weight: 3, label: 'Abandoned Building', icon: 'fa-building-circle-xmark' },-- disused: { query: '["disused"="yes"]["building"]', weight: 2, label: 'Disused Building', icon: 'fa-building' },++ abandoned: { query: '["abandoned"="yes"]', weight: 3, label: 'Abandoned Building', icon: 'fa-circle-xmark' },++ disused: { query: '["disused"="yes"]["building"]', weight: 2, label: 'Disused Building', icon: 'fa-city' },};// ── Curated WA Sketchy / High-Activity Zones ────────────────────────@@@ -73,42 +73,42 @@ const STORES = {const FOOD_GROUPS = {protein: {name: 'Protein',-- icon: 'fa-drumstick-bite',++ icon: 'fa-fish',color: '#9b59b6',dailyMin: 50, // gramsexamples: 'Chicken, eggs, beans, peanut butter, canned tuna',},grains: {name: 'Grains',-- icon: 'fa-wheat-awn',++ icon: 'fa-seedling',color: '#e67e22',dailyMin: 170, // grams (6 oz equiv)examples: 'Rice, bread, pasta, oatmeal, tortillas',},vegetables: {name: 'Vegetables',-- icon: 'fa-carrot',++ icon: 'fa-leaf',color: '#27ae60',dailyMin: 300, // grams (~2.5 cups)examples: 'Carrots, potatoes, canned corn, frozen mixed veggies',},fruits: {name: 'Fruits',-- icon: 'fa-apple-whole',++ icon: 'fa-utensils',color: '#e74c3c',dailyMin: 200, // grams (~2 cups)examples: 'Bananas, apples, oranges, canned fruit',},dairy: {name: 'Dairy',-- icon: 'fa-glass-water',++ icon: 'fa-mug-saucer',color: '#3498db',dailyMin: 720, // ml (~3 cups)examples: 'Milk, cheese, yogurt',},fats: {name: 'Fats & Oils',-- icon: 'fa-oil-can',++ icon: 'fa-droplet',color: '#f1c40f',dailyMin: 25, // gramsexamples: 'Cooking oil, butter, peanut butter, avocado',@@@ -864,9 +864,9 @@ function formatOSMAddress(tags) {// ─── Find Food Banks, Soup Kitchens, Community Fridges ─────────────const FOOD_BANK_TYPES = {-- food_bank: { label: 'Food Bank', icon: 'fa-boxes-stacked', color: '#ef4444' },-- soup_kitchen: { label: 'Soup Kitchen', icon: 'fa-bowl-food', color: '#f97316' },-- food_sharing: { label: 'Community Fridge', icon: 'fa-temperature-low', color: '#06b6d4' },++ food_bank: { label: 'Food Bank', icon: 'fa-cubes', color: '#ef4444' },++ soup_kitchen: { label: 'Soup Kitchen', icon: 'fa-utensils', color: '#f97316' },++ food_sharing: { label: 'Community Fridge', icon: 'fa-snowflake', color: '#06b6d4' },give_box: { label: 'Free Pantry Box', icon: 'fa-box-open', color: '#8b5cf6' },};@@@ -18,18 +18,18 @@ const { haversine, overpassQuery } = require('./utils');// HARM REDUCTION CATEGORIES// ═══════════════════════════════════════════════════════════════════const HR_TYPES = {-- needle_exchange: { label: 'Syringe Exchange', icon: 'fa-syringe', color: '#dc2626' },++ needle_exchange: { label: 'Syringe Exchange', icon: 'fa-briefcase-medical', color: '#dc2626' },condom_dist: { label: 'Condom Distribution', icon: 'fa-heart', color: '#ec4899' },naloxone: { label: 'Naloxone / Narcan', icon: 'fa-kit-medical', color: '#f97316' },harm_reduction: { label: 'Harm Reduction Center', icon: 'fa-hand-holding-heart', color: '#8b5cf6' },addiction_counsel: { label: 'Addiction Counselling', icon: 'fa-comments', color: '#3b82f6' },-- hiv_testing: { label: 'HIV / STI Testing', icon: 'fa-vial', color: '#ef4444' },-- sexual_health: { label: 'Sexual Health Clinic', icon: 'fa-stethoscope', color: '#0891b2' },-- family_planning: { label: 'Family Planning', icon: 'fa-baby', color: '#a855f7' },-- vending_health: { label: 'Health Vending Machine', icon: 'fa-cash-register', color: '#22c55e' },++ hiv_testing: { label: 'HIV / STI Testing', icon: 'fa-flask-vial', color: '#ef4444' },++ sexual_health: { label: 'Sexual Health Clinic', icon: 'fa-kit-medical', color: '#0891b2' },++ family_planning: { label: 'Family Planning', icon: 'fa-child', color: '#a855f7' },++ vending_health: { label: 'Health Vending Machine', icon: 'fa-money-bill', color: '#22c55e' },outreach: { label: 'Outreach Program', icon: 'fa-people-arrows', color: '#10b981' },-- rehab: { label: 'Rehab / Recovery', icon: 'fa-house-medical', color: '#6366f1' },-- social_health: { label: 'Community Health Services', icon: 'fa-hospital', color: '#0ea5e9' },++ rehab: { label: 'Rehab / Recovery', icon: 'fa-house', color: '#6366f1' },++ social_health: { label: 'Community Health Services', icon: 'fa-kit-medical', color: '#0ea5e9' },};// ═══════════════════════════════════════════════════════════════════@@@ -15,16 +15,16 @@ const CATEGORIES = {'Informal Campsite': { type: 'Informal Camp', stealth: 4, icon: 'fa-campground' },'Established Campground': { type: 'Campground', stealth: 2, icon: 'fa-campground' },'Overnight Parking': { type: 'Overnight Parking', stealth: 3, icon: 'fa-parking' },-- 'Rest Area': { type: 'Rest Area', stealth: 2, icon: 'fa-bed' },-- 'Gas Station': { type: 'Services', stealth: 1, icon: 'fa-gas-pump' },++ 'Rest Area': { type: 'Rest Area', stealth: 2, icon: 'fa-moon' },++ 'Gas Station': { type: 'Services', stealth: 1, icon: 'fa-store' },'Water': { type: 'Water Source', stealth: 0, icon: 'fa-faucet-drip' },-- 'Dump Station': { type: 'Services', stealth: 0, icon: 'fa-dumpster' },-- 'Propane': { type: 'Services', stealth: 0, icon: 'fa-fire-flame-simple' },++ 'Dump Station': { type: 'Services', stealth: 0, icon: 'fa-trash' },++ 'Propane': { type: 'Services', stealth: 0, icon: 'fa-fire' },'Mechanic': { type: 'Services', stealth: 0, icon: 'fa-wrench' },'Wifi': { type: 'Services', stealth: 0, icon: 'fa-wifi' },'Laundry': { type: 'Services', stealth: 0, icon: 'fa-shirt' },-- 'Hostel': { type: 'Shelter', stealth: 1, icon: 'fa-bed' },-- 'Hotel': { type: 'Shelter', stealth: 1, icon: 'fa-hotel' },++ 'Hostel': { type: 'Shelter', stealth: 1, icon: 'fa-moon' },++ 'Hotel': { type: 'Shelter', stealth: 1, icon: 'fa-couch' },};/**@@@ -89,7 +89,7 @@ async function search(lat, lon, radiusMiles, apiKey) {distanceMiles: Math.round(dist * 10) / 10,type: 'EV Charging',source: 'OpenChargeMap',-- sourceIcon: 'fa-charging-station',++ sourceIcon: 'fa-bolt',reservable: false,url: `https://openchargemap.org/site/poi/details/${s.ID}`,fee: isFree ? 'Free' : (s.UsageCost || 'Check station'),@@@ -18,9 +18,9 @@ const { haversine, OVERPASS_URL, overpassQuery } = require('./utils');// ═══════════════════════════════════════════════════════════════════const RESOURCE_TYPES = {library: { label: 'Library', icon: 'fa-book', color: '#8b5cf6' },-- shower: { label: 'Public Shower', icon: 'fa-shower', color: '#06b6d4' },++ shower: { label: 'Public Shower', icon: 'fa-faucet', color: '#06b6d4' },laundry: { label: 'Laundromat', icon: 'fa-shirt', color: '#f59e0b' },-- community: { label: 'Community Center', icon: 'fa-people-roof', color: '#ec4899' },++ community: { label: 'Community Center', icon: 'fa-people-group', color: '#ec4899' },water: { label: 'Drinking Water', icon: 'fa-faucet-drip', color: '#3b82f6' },food_bank: { label: 'Food Bank', icon: 'fa-utensils', color: '#ef4444' },social: { label: 'Social Services', icon: 'fa-hand-holding-heart', color: '#10b981' },@@@ -30,19 +30,19 @@ const RESOURCE_TYPES = {bottle_return: { label: 'Bottle Return', icon: 'fa-recycle', color: '#22c55e' },clinic: { label: 'Free Clinic', icon: 'fa-kit-medical', color: '#dc2626' },homeless_shelter: { label: 'Homeless Shelter', icon: 'fa-house-chimney', color: '#7c3aed' },-- hospital: { label: 'Hospital / ER', icon: 'fa-hospital', color: '#be123c' },-- pharmacy: { label: 'Pharmacy', icon: 'fa-prescription', color: '#0891b2' },++ hospital: { label: 'Hospital / ER', icon: 'fa-kit-medical', color: '#be123c' },++ pharmacy: { label: 'Pharmacy', icon: 'fa-suitcase-medical', color: '#0891b2' },post_office: { label: 'Post Office', icon: 'fa-envelope', color: '#4338ca' },thrift_store: { label: 'Thrift / Free Store', icon: 'fa-shirt', color: '#a16207' },public_bookcase: { label: 'Little Free Library', icon: 'fa-book-open', color: '#92400e' },water_point: { label: 'Water Point (Tank)', icon: 'fa-faucet', color: '#0284c7' },-- place_of_worship: { label: 'Church / Worship', icon: 'fa-church', color: '#6366f1' },-- bus_station: { label: 'Bus / Transit Stop', icon: 'fa-bus', color: '#0ea5e9' },-- gas_station: { label: 'Gas Station', icon: 'fa-gas-pump', color: '#f43f5e' },-- clothing_bank: { label: 'Clothing Bank', icon: 'fa-vest', color: '#d946ef' },++ place_of_worship: { label: 'Church / Worship', icon: 'fa-house-chimney', color: '#6366f1' },++ bus_station: { label: 'Bus / Transit Stop', icon: 'fa-train', color: '#0ea5e9' },++ gas_station: { label: 'Gas Station', icon: 'fa-store', color: '#f43f5e' },++ clothing_bank: { label: 'Clothing Bank', icon: 'fa-shirt', color: '#d946ef' },day_shelter: { label: 'Day Shelter / Drop-In',icon: 'fa-sun', color: '#fb923c' },pet_friendly: { label: 'Pet-Friendly Shelter', icon: 'fa-paw', color: '#84cc16' },-- warming_center: { label: 'Warming / Cooling', icon: 'fa-temperature-half', color: '#ef4444' },++ warming_center: { label: 'Warming / Cooling', icon: 'fa-sun', color: '#ef4444' },};// ═══════════════════════════════════════════════════════════════════@@@ -195,11 +195,11 @@ out center body;`;stealthRating = 2;} else if (tags.amenity === 'rest_area' || tags.highway === 'rest_area') {type = 'Rest Area';-- icon = 'fa-bed';++ icon = 'fa-moon';stealthRating = 3;} else if (tags.tourism === 'caravan_site') {type = 'RV/Caravan Site';-- icon = 'fa-caravan';++ icon = 'fa-car';stealthRating = 3;}@@@ -50,8 +50,8 @@ const locations = [{ name: 'Stillaguamish River – South Fork at Jim Creek', lat: 48.1350, lon: -121.9700, type: 'Swimming Hole', source: 'Built-in DB', sourceIcon: 'fa-person-swimming', description: 'South Fork Stillaguamish swimming access near Jim Creek area. Forest road pulloffs lead to river. Cold, clean water. Rock pools.', stealthRating: 4, fee: 'Free', amenities: ['River Swimming', 'Clean Water', 'Forest Setting', 'Rock Pools'] },// ── Arlington/Smokey Point Services & Resources ──{ name: 'Arlington Community Food Bank', lat: 48.1684, lon: -122.1465, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-utensils', description: 'Free food bank at 19118 63rd Ave NE. Open Tue/Wed/Fri 12:30-3:30 PM. Fresh produce, canned goods, bread, dairy. No questions asked. Serves North Snohomish County.', stealthRating: 0, fee: 'Free', amenities: ['Food Bank', 'Fresh Produce', 'No ID Required'] },-- { name: 'Village Community Services – Smokey Point', lat: 48.1547, lon: -122.1864, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-hands-helping', description: 'Social services at 3210 Smokey Point Dr (Royal Pacific Bldg). Emergency assistance, food, clothing, case management. Mon-Fri 8AM-5PM. Serves homeless individuals.', stealthRating: 0, fee: 'Free', amenities: ['Emergency Aid', 'Food', 'Clothing', 'Case Management'] },-- { name: 'VOA Arlington Resource Center', lat: 48.1951, lon: -122.1269, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-hands-helping', description: 'Volunteers of America resource center at 103 E 3rd St in downtown Arlington. Housing assistance, case management, community resources. Mon-Fri 9AM-5PM.', stealthRating: 0, fee: 'Free', amenities: ['Housing Help', 'Case Management', 'Community Resources'] },++ { name: 'Village Community Services – Smokey Point', lat: 48.1547, lon: -122.1864, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-handshake', description: 'Social services at 3210 Smokey Point Dr (Royal Pacific Bldg). Emergency assistance, food, clothing, case management. Mon-Fri 8AM-5PM. Serves homeless individuals.', stealthRating: 0, fee: 'Free', amenities: ['Emergency Aid', 'Food', 'Clothing', 'Case Management'] },++ { name: 'VOA Arlington Resource Center', lat: 48.1951, lon: -122.1269, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-handshake', description: 'Volunteers of America resource center at 103 E 3rd St in downtown Arlington. Housing assistance, case management, community resources. Mon-Fri 9AM-5PM.', stealthRating: 0, fee: 'Free', amenities: ['Housing Help', 'Case Management', 'Community Resources'] },{ name: 'Salt of the Earth Food Blessing', lat: 48.1548, lon: -122.1856, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-utensils', description: 'Free food distribution at 3310 Smokey Point Dr. Faith-based food blessing program. Open select days. No ID or proof of need required.', stealthRating: 0, fee: 'Free', amenities: ['Food Distribution', 'No ID Required'] },{ name: 'Walmart Supercenter – Smokey Point', lat: 48.1507, lon: -122.1746, type: 'Urban Stealth', source: 'Built-in DB', sourceIcon: 'fa-map-pin', description: 'Walmart at 4010 172nd St NE near Smokey Point. 24-hour access. Restrooms, car camping in lot (ask mgmt). Hot food deli, pharmacy, supplies.', stealthRating: 2, fee: 'Free', amenities: ['24hr Restrooms', 'Car Camping', 'Supplies', 'Pharmacy', 'Hot Food'] },{ name: 'Grocery Outlet – Arlington', lat: 48.2008, lon: -122.1259, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-map-pin', description: 'Discount grocery at 123 E Burke Ave in downtown Arlington. Cheap food, produce, essentials. Walking distance from Centennial Trail.', stealthRating: 0, fee: 'Discount', amenities: ['Cheap Food', 'Downtown', 'Near Trail'] },@@@ -141,7 +141,7 @@ const locations = [{ name: 'Centennial Trail Bridge – Stillaguamish', lat: 48.1975, lon: -122.1200, type: 'Bridge', source: 'Built-in DB', sourceIcon: 'fa-bridge', description: 'RIVER BRIDGE: Centennial Trail pedestrian/bike bridge over Stillaguamish River near downtown Arlington. Great views. River access from trail.', stealthRating: 2, fee: 'Free', amenities: ['Pedestrian Bridge', 'Trail Access', 'River Views'] },// ── Arlington/Smokey Point – SHOWERS ──-- { name: 'Planet Fitness – Smokey Point', lat: 48.1510, lon: -122.1793, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-shower', description: 'Planet Fitness gym at 3713 172nd St NE. $10/month membership gives 24/7 access to showers, lockers, WiFi. Clean facilities. Day pass sometimes available.', stealthRating: 0, fee: '$10/mo', amenities: ['24/7 Showers', 'WiFi', 'Lockers', 'Climate Control'] },++ { name: 'Planet Fitness – Smokey Point', lat: 48.1510, lon: -122.1793, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-faucet', description: 'Planet Fitness gym at 3713 172nd St NE. $10/month membership gives 24/7 access to showers, lockers, WiFi. Clean facilities. Day pass sometimes available.', stealthRating: 0, fee: '$10/mo', amenities: ['24/7 Showers', 'WiFi', 'Lockers', 'Climate Control'] },{ name: 'Cascade Valley Hospital – ER Lobby', lat: 48.1967, lon: -122.1192, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-map-pin', description: 'Cascade Valley Hospital ER lobby is 24/7 accessible. Warm waiting area, restrooms, water fountain. Social workers available during business hours for housing/resource referrals.', stealthRating: 0, fee: 'Free lobby', amenities: ['24hr Lobby', 'Restrooms', 'Water', 'Warm', 'Social Worker'] },// ── Arlington – FOREST AREAS & NATURE (near 43rd Ave NE) ──@@@ -42,7 +42,7 @@ async function getBulkElevation(coords) {function classifyElevation(elev) {if (elev === null) return { class: 'unknown', label: 'Unknown', icon: 'fa-question' };if (elev < 100) return { class: 'lowland', label: 'Lowland/Valley', icon: 'fa-water', note: 'Near sea level. May flood in winter.' };-- if (elev < 500) return { class: 'foothills', label: 'Foothills', icon: 'fa-hill-rockslide', note: 'Moderate elevation. Good year-round camping.' };++ if (elev < 500) return { class: 'foothills', label: 'Foothills', icon: 'fa-mountain', note: 'Moderate elevation. Good year-round camping.' };if (elev < 1500) return { class: 'mountain', label: 'Mountain', icon: 'fa-mountain', note: 'Mountain terrain. Snow possible Oct-May.' };if (elev < 2500) return { class: 'alpine', label: 'High Mountain', icon: 'fa-mountain-sun', note: 'High altitude. Snow likely Nov-Jun. Be prepared for weather.' };return { class: 'alpine-extreme', label: 'Alpine/Extreme', icon: 'fa-mountain-sun', note: 'Very high altitude. Extreme conditions. Summer access only.' };@@@ -28,14 +28,14 @@ const ACTIVITY_MAP = {'Picnicking': { type: 'USFS Picnic Area', icon: 'fa-utensils', stealth: 2 },'Fishing': { type: 'Fishing Access', icon: 'fa-fish', stealth: 3 },'Swimming': { type: 'Swimming Area', icon: 'fa-water', stealth: 2 },-- 'Boating': { type: 'Boat Launch', icon: 'fa-sailboat', stealth: 2 },-- 'Mountain Biking': { type: 'MTB Trailhead', icon: 'fa-bicycle', stealth: 3 },++ 'Boating': { type: 'Boat Launch', icon: 'fa-water', stealth: 2 },++ 'Mountain Biking': { type: 'MTB Trailhead', icon: 'fa-road', stealth: 3 },'Winter Sports': { type: 'Winter Sports Area', icon: 'fa-snowflake', stealth: 2 },-- 'OHV/Off-Highway Driving': { type: 'OHV Area', icon: 'fa-truck-monster', stealth: 4 },-- 'Horseback Riding': { type: 'Equestrian Trailhead', icon: 'fa-horse', stealth: 3 },-- 'Wildlife Viewing': { type: 'Wildlife Viewing', icon: 'fa-binoculars', stealth: 3 },++ 'OHV/Off-Highway Driving': { type: 'OHV Area', icon: 'fa-car', stealth: 4 },++ 'Horseback Riding': { type: 'Equestrian Trailhead', icon: 'fa-paw', stealth: 3 },++ 'Wildlife Viewing': { type: 'Wildlife Viewing', icon: 'fa-eye', stealth: 3 },'Visitor Centers': { type: 'Visitor Center', icon: 'fa-info-circle', stealth: 1 },-- 'Nature Viewing': { type: 'Nature Viewing', icon: 'fa-binoculars', stealth: 3 },++ 'Nature Viewing': { type: 'Nature Viewing', icon: 'fa-eye', stealth: 3 },'Interpretive Programs': { type: 'Interpretive Site', icon: 'fa-book-open', stealth: 1 },'Climbing': { type: 'Climbing Area', icon: 'fa-mountain', stealth: 4 },'Water Sports': { type: 'Water Sports', icon: 'fa-water', stealth: 2 },@@@ -46,8 +46,8 @@ const WATERWAY_TYPES = {ford: { label: 'Ford / River Crossing',icon: 'fa-road', color: '#0891b2', stealth: 4 },fishing: { label: 'Fishing Area', icon: 'fa-fish', color: '#059669', stealth: 3 },swimming: { label: 'Swimming Area', icon: 'fa-person-swimming', color: '#14b8a6', stealth: 3 },-- boat_launch: { label: 'Boat Launch / Slipway',icon: 'fa-ship', color: '#0d9488', stealth: 2 },-- pier: { label: 'Pier / Dock', icon: 'fa-anchor', color: '#64748b', stealth: 2 },++ boat_launch: { label: 'Boat Launch / Slipway',icon: 'fa-water', color: '#0d9488', stealth: 2 },++ pier: { label: 'Pier / Dock', icon: 'fa-map-pin', color: '#64748b', stealth: 2 },wetland: { label: 'Wetland', icon: 'fa-water', color: '#84cc16', stealth: 3 },river_area: { label: 'River Area', icon: 'fa-water', color: '#2563eb', stealth: 4 },beach: { label: 'Beach', icon: 'fa-umbrella-beach', color: '#fbbf24', stealth: 3 },@@@ -31,7 +31,7 @@ const WMO_CODES = {73: { description: 'Moderate snow', icon: 'fa-snowflake', cls: 'snow' },75: { description: 'Heavy snow', icon: 'fa-snowflake', cls: 'snow' },77: { description: 'Snow grains', icon: 'fa-snowflake', cls: 'snow' },-- 80: { description: 'Light showers', icon: 'fa-cloud-sun-rain', cls: 'rain' },++ 80: { description: 'Light showers', icon: 'fa-cloud-rain', cls: 'rain' },81: { description: 'Moderate showers', icon: 'fa-cloud-showers-heavy', cls: 'rain' },82: { description: 'Violent showers', icon: 'fa-cloud-showers-heavy', cls: 'rain' },85: { description: 'Light snow showers', icon: 'fa-snowflake', cls: 'snow' },@@@ -47,7 +47,7 @@ function decodeWMO(code, isDay) {let icon = entry.icon;if (!isDay && icon === 'fa-sun') icon = 'fa-moon';if (!isDay && icon === 'fa-cloud-sun') icon = 'fa-cloud-moon';-- if (!isDay && icon === 'fa-cloud-sun-rain') icon = 'fa-cloud-moon-rain';++ if (!isDay && icon === 'fa-cloud-rain') icon = 'fa-cloud-moon';return { ...entry, icon };}@@@ -18,7 +18,7 @@"helmet": "^7.1.0","iovalkey": "^0.4.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0","pg": "^8.23.0"}},@@@ -1130,9 +1130,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.56.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",++ "version": "4.73.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script><style>@@@ -47,7 +46,7 @@<body><div class="tool-wrap" style="max-width: 1000px;"><div class="page-header">-- <h1><i class="fas fa-certificate"></i> Badge & Sticker Maker</h1>++ <h1><i class="fas fa-ranking-star"></i> Badge & Sticker Maker</h1><p class="subtitle">Design badges, stickers, and labels with custom shapes, colors, and fonts.</p></div>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head>@@@ -42,7 +41,7 @@<div class="btn-row"><a href="https://fancy.openvibe.tools" class="btn btn-secondary"><i class="fas fa-wand-magic-sparkles"></i> Fancy Text</a><a href="https://symbols.openvibe.tools" class="btn btn-secondary"><i class="fas fa-icons"></i> Symbols</a>-- <a href="https://kaomoji.openvibe.tools" class="btn btn-secondary"><i class="fas fa-face-smile-beam"></i> Kaomoji</a>++ <a href="https://kaomoji.openvibe.tools" class="btn btn-secondary"><i class="fas fa-face-smile"></i> Kaomoji</a></div></div>@@@ -18,14 +18,13 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head><body><div class="tool-wrap"><div class="page-header">-- <h1><i class="fas fa-braille"></i> Braille Translator</h1>++ <h1><i class="fas fa-grip"></i> Braille Translator</h1><p class="subtitle">Convert text to Braille Unicode dots and decode Braille back to text.</p></div>@@@ -45,7 +44,7 @@</div><div class="tool-section">-- <h2><i class="fas fa-braille"></i> Output</h2>++ <h2><i class="fas fa-grip"></i> Output</h2><div id="output" class="output-box mono" style="font-size: 1.4rem; letter-spacing: 0.2em;"></div><div class="btn-row"><button class="btn btn-primary" onclick="OpenVibeUI.copyText(document.getElementById('output').textContent)"><i class="fas fa-copy"></i> Copy</button>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head>@@@ -18,14 +18,13 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head><body><div class="tool-wrap"><div class="page-header">-- <h1><i class="fas fa-text-slash"></i> Case Converter</h1>++ <h1><i class="fas fa-font"></i> Case Converter</h1><p class="subtitle">Instantly convert text between 14 different case styles. Click any result to copy.</p></div>@@@ -18,14 +18,13 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head><body><div class="tool-wrap"><div class="page-header">-- <h1><i class="fas fa-broom"></i> Text Cleaner</h1>++ <h1><i class="fas fa-eraser"></i> Text Cleaner</h1><p class="subtitle">Clean up messy text with one click. Fix quotes, strip invisible characters, normalize spacing.</p></div>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script><style>@@@ -38,7 +37,7 @@<body><div class="tool-wrap" style="max-width: 1000px;"><div class="page-header">-- <h1><i class="fas fa-code-compare"></i> Text Compare</h1>++ <h1><i class="fas fa-arrows-left-right"></i> Text Compare</h1><p class="subtitle">Paste two texts side by side and see the differences highlighted line by line.</p></div>@@@ -18,14 +18,13 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head><body><div class="tool-wrap"><div class="page-header">-- <h1><i class="fas fa-calculator"></i> Text Counter</h1>++ <h1><i class="fas fa-table-cells"></i> Text Counter</h1><p class="subtitle">Real-time character, word, sentence, and paragraph counting with reading & speaking time.</p></div>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script><style>@@@ -62,7 +61,7 @@<script>OpenVibeUI.initPage();-- const ICONS = { htmlEscape: 'fa-code', htmlUnescape: 'fa-code', urlEncode: 'fa-link', urlDecode: 'fa-link', base64Encode: 'fa-lock', base64Decode: 'fa-unlock', jsonStringify: 'fa-brackets-curly', jsonParse: 'fa-brackets-curly', cssEscape: 'fa-paintbrush', regexEscape: 'fa-asterisk', unicodeEscape: 'fa-globe', hexEncode: 'fa-hashtag' };++ const ICONS = { htmlEscape: 'fa-code', htmlUnescape: 'fa-code', urlEncode: 'fa-link', urlDecode: 'fa-link', base64Encode: 'fa-lock', base64Decode: 'fa-unlock', jsonStringify: 'fa-code', jsonParse: 'fa-code', cssEscape: 'fa-paintbrush', regexEscape: 'fa-star', unicodeEscape: 'fa-globe', hexEncode: 'fa-hashtag' };const TRANSFORMS = Object.entries(TextOpenVibeEngine.escapes).map(([id, e]) => ({ id, label: e.label, icon: ICONS[id], fn: e.fn }));const grid = document.getElementById('grid');@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script><style>@@@ -117,12 +116,16 @@}.scenario summary::-webkit-details-marker { display: none; }.scenario summary::after {-- content: '\f054';-- font-family: 'Font Awesome 6 Free';-- font-weight: 900;-- font-size: 0.75rem;++ /* The OpenVibe chevron (openvibe.design/icons/chevron), painted in the text's colour. */++ content: '';++ width: 0.75rem;++ height: 0.75rem;++ flex: none;margin-left: auto;color: var(--text-muted);++ background-color: currentColor;++ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;++ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;transition: transform 0.2s;}.scenario[open] summary::after { transform: rotate(90deg); }@@@ -187,7 +190,7 @@<!-- ═══ TAB: KNOW YOUR RIGHTS ═══ --><div class="tab-content active" id="tab-rights"><div class="tool-section">-- <h2><i class="fas fa-landmark"></i> Your Constitutional Rights</h2>++ <h2><i class="fas fa-gavel"></i> Your Constitutional Rights</h2><p style="color:var(--text-secondary); margin-bottom:16px;">Regardless of immigration status, the U.S. Constitution guarantees these rights to all people on U.S. soil.</p><div class="rights-grid">@@@ -200,7 +203,7 @@<p>You have the right to speak with an attorney. If detained, say: <strong>"I want to speak with a lawyer before answering any questions."</strong> You have the right to make a phone call. Immigration detainees must be given a list of free legal service providers.</p></div><div class="rights-card">-- <h3><i class="fas fa-house-lock"></i> Protection in Your Home</h3>++ <h3><i class="fas fa-house"></i> Protection in Your Home</h3><p>ICE generally <strong>cannot enter your home without a judicial warrant</strong> signed by a judge. An ICE administrative warrant (Form I-200) does NOT give them permission to enter. You are not required to open the door. Ask them to slip the warrant under the door.</p></div><div class="rights-card">@@@ -208,7 +211,7 @@<p>You can refuse a search of yourself, your car, or your home. Say clearly: <strong>"I do not consent to a search."</strong> This does not guarantee they won't search, but it protects your legal rights.</p></div><div class="rights-card">-- <h3><i class="fas fa-file-signature"></i> Don't Sign Anything</h3>++ <h3><i class="fas fa-file-pen"></i> Don't Sign Anything</h3><p><strong>Do not sign any documents</strong> without reading them carefully and speaking to a lawyer first. Signing voluntary departure papers waives your right to appear before an immigration judge and could result in a 10-year bar on re-entry.</p></div><div class="rights-card">@@@ -266,7 +269,7 @@</details><details class="scenario">-- <summary><i class="fas fa-building"></i> ICE at Your Workplace</summary>++ <summary><i class="fas fa-city"></i> ICE at Your Workplace</summary><div class="scenario-body"><ul><li>Your employer should ask to see a <strong>judicial warrant</strong> before allowing ICE into non-public areas.</li>@@@ -292,7 +295,7 @@</details><details class="scenario">-- <summary><i class="fas fa-handcuffs"></i> If You Are Detained</summary>++ <summary><i class="fas fa-lock"></i> If You Are Detained</summary><div class="scenario-body"><ul><li>Stay calm and repeat: <em>"I wish to remain silent. I want to speak with a lawyer."</em></li>@@@ -348,7 +351,7 @@<!-- ═══ TAB: RESOURCES ═══ --><div class="tab-content" id="tab-resources"><div class="tool-section">-- <h2><i class="fas fa-phone-volume"></i> Emergency Hotlines</h2>++ <h2><i class="fas fa-phone"></i> Emergency Hotlines</h2><div class="emergency-grid"><div class="emergency-card"><div class="em-icon red"><i class="fas fa-phone"></i></div>@@@ -365,7 +368,7 @@</div></div><div class="emergency-card">-- <div class="em-icon green"><i class="fas fa-hands-holding-child"></i></div>++ <div class="em-icon green"><i class="fas fa-child"></i></div><div class="em-info"><h4>RAICES Texas</h4><p><a href="tel:1-800-732-4237">1-800-732-4237</a> — Free legal help</p>@@@ -414,9 +417,9 @@<div class="resource-links"><a class="resource-link" href="https://www.immigrationadvocates.org/legaldirectory/" target="_blank" rel="noopener"><i class="fas fa-search"></i> Find Free Legal Help Near You</a><a class="resource-link" href="https://www.aclu.org/know-your-rights/immigrants-rights" target="_blank" rel="noopener"><i class="fas fa-shield-halved"></i> ACLU Know Your Rights</a>-- <a class="resource-link" href="https://www.raicestexas.org/know-your-rights/" target="_blank" rel="noopener"><i class="fas fa-hands-holding-child"></i> RAICES Texas</a>++ <a class="resource-link" href="https://www.raicestexas.org/know-your-rights/" target="_blank" rel="noopener"><i class="fas fa-child"></i> RAICES Texas</a><a class="resource-link" href="https://unitedwedream.org/our-work/deportation-defense/know-your-rights/" target="_blank" rel="noopener"><i class="fas fa-fist-raised"></i> United We Dream</a>-- <a class="resource-link" href="https://www.nilc.org/" target="_blank" rel="noopener"><i class="fas fa-landmark"></i> National Immigration Law Center</a>++ <a class="resource-link" href="https://www.nilc.org/" target="_blank" rel="noopener"><i class="fas fa-gavel"></i> National Immigration Law Center</a><a class="resource-link" href="https://cliniclegal.org/" target="_blank" rel="noopener"><i class="fas fa-scale-balanced"></i> CLINIC Legal</a><a class="resource-link" href="https://www.informedimmigrant.com/" target="_blank" rel="noopener"><i class="fas fa-book"></i> Informed Immigrant</a></div>@@@ -426,7 +429,7 @@<h2><i class="fas fa-mobile-screen"></i> Apps & Tools</h2><div class="rights-grid"><div class="rights-card">-- <h3><i class="fab fa-app-store"></i> Know Your Rights App (NAKASEC)</h3>++ <h3><i class="fas fa-mobile-screen"></i> Know Your Rights App (NAKASEC)</h3><p>Mobile app for immigrants to understand their legal rights. Available on iOS.</p><div class="rights-lang"><span class="lang-tag">English</span><span class="lang-tag">Español</span><span class="lang-tag">한국어</span></div></div>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head>@@@ -69,7 +68,7 @@<span class="card-domain">ascii.openvibe.tools</span></a><a href="https://case.openvibe.tools" class="tool-card">-- <div class="card-icon"><i class="fas fa-text-slash"></i></div>++ <div class="card-icon"><i class="fas fa-font"></i></div><h3>Case Converter</h3><p>UPPER, lower, Title, camelCase, snake_case, kebab-case, PascalCase, mOcKiNg, and more.</p><span class="card-domain">case.openvibe.tools</span>@@@ -87,13 +86,13 @@<span class="card-domain">bubble.openvibe.tools</span></a><a href="https://count.openvibe.tools" class="tool-card">-- <div class="card-icon"><i class="fas fa-calculator"></i></div>++ <div class="card-icon"><i class="fas fa-table-cells"></i></div><h3>Text Counter</h3><p>Count characters, words, sentences, paragraphs, bytes. Reading time and speaking time estimates.</p><span class="card-domain">count.openvibe.tools</span></a><a href="https://clean.openvibe.tools" class="tool-card">-- <div class="card-icon"><i class="fas fa-broom"></i></div>++ <div class="card-icon"><i class="fas fa-eraser"></i></div><h3>Text Cleaner</h3><p>Fix smart quotes, remove invisible characters, strip Zalgo, normalize spacing, clean HTML tags.</p><span class="card-domain">clean.openvibe.tools</span>@@@ -105,7 +104,7 @@<span class="card-domain">reversetext.openvibe.tools</span></a><a href="https://kaomoji.openvibe.tools" class="tool-card">-- <div class="card-icon"><i class="fas fa-face-smile-beam"></i></div>++ <div class="card-icon"><i class="fas fa-face-smile"></i></div><h3>Kaomoji & Emoticons</h3><p>Browse and copy Japanese text emoticons sorted by mood: happy, sad, angry, love, shrug, lenny, and more.</p><span class="card-domain">kaomoji.openvibe.tools</span>@@@ -141,13 +140,13 @@<span class="card-domain">binary.openvibe.tools</span></a><a href="https://markdown.openvibe.tools" class="tool-card">-- <div class="card-icon"><i class="fab fa-markdown"></i></div>++ <div class="card-icon"><i class="fas fa-code"></i></div><h3>Markdown Preview</h3><p>Write Markdown on the left, see rendered HTML on the right. Export as HTML.</p><span class="card-domain">markdown.openvibe.tools</span></a><a href="https://sort.openvibe.tools" class="tool-card">-- <div class="card-icon"><i class="fas fa-sort-alpha-down"></i></div>++ <div class="card-icon"><i class="fas fa-arrow-down-short-wide"></i></div><h3>Sort & Dedupe Lines</h3><p>Sort lines alphabetically, numerically, or randomly. Remove duplicates instantly.</p><span class="card-domain">sort.openvibe.tools</span>@@@ -191,7 +190,7 @@<span class="card-domain">wordmark.openvibe.tools</span></a><a href="https://badge.openvibe.tools" class="tool-card">-- <div class="card-icon"><i class="fas fa-certificate"></i></div>++ <div class="card-icon"><i class="fas fa-ranking-star"></i></div><h3>Badge & Sticker Maker</h3><p>Create badges, stickers, and labels with rounded shapes, bold text, and vibrant colors.</p><span class="card-domain">badge.openvibe.tools</span>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script><style>@@@ -65,7 +64,7 @@<button class="btn btn-primary" onclick="beautify()"><i class="fas fa-wand-magic-sparkles"></i> Beautify</button><button class="btn btn-secondary" onclick="minify()"><i class="fas fa-compress"></i> Minify</button><button class="btn btn-secondary" onclick="OpenVibeUI.copyText(lastFormatted)"><i class="fas fa-copy"></i> Copy Output</button>-- <button class="btn btn-secondary" onclick="sortKeys()"><i class="fas fa-arrow-down-a-z"></i> Sort Keys</button>++ <button class="btn btn-secondary" onclick="sortKeys()"><i class="fas fa-arrow-down-wide-short"></i> Sort Keys</button></div></div>@@@ -18,14 +18,13 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head><body><div class="tool-wrap" style="max-width: 1000px;"><div class="page-header">-- <h1><i class="fas fa-face-smile-beam"></i> Kaomoji</h1>++ <h1><i class="fas fa-face-smile"></i> Kaomoji</h1><p class="subtitle">Japanese text emoticons sorted by mood. Click any to copy. (◕‿◕)</p></div>@@@ -44,11 +43,11 @@OpenVibeUI.initPage();const container = document.getElementById('categories');const moodIcons = {-- happy:'fa-face-smile-beam', sad:'fa-face-sad-tear', angry:'fa-face-angry',-- love:'fa-heart', shrug:'fa-person-shrugging', surprise:'fa-face-surprise',-- cool:'fa-sunglasses', silly:'fa-face-grin-tongue', animals:'fa-paw',++ happy:'fa-face-smile', sad:'fa-face-grin-squint-tears', angry:'fa-face-grin-squint',++ love:'fa-heart', shrug:'fa-question', surprise:'fa-face-grin-stars',++ cool:'fa-eye', silly:'fa-face-laugh', animals:'fa-paw',music:'fa-music', tableFlip:'fa-table', tableUnflip:'fa-table',-- lenny:'fa-face-grin-wink', fight:'fa-hand-fist', sparkle:'fa-sparkles',++ lenny:'fa-face-grin', fight:'fa-hand-fist', sparkle:'fa-sparkles',};for (const [mood, items] of Object.entries(TextOpenVibeEngine.kaomoji)) {@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head>@@@ -53,7 +52,7 @@<!-- Tools grid --><div class="section-header">-- <i class="fas fa-tools"></i>++ <i class="fas fa-screwdriver-wrench"></i><h2>Design Tools</h2></div><div class="tool-grid">@@@ -70,7 +69,7 @@<span class="card-domain">wordmark.openvibe.tools</span></a><a href="https://badge.openvibe.tools" class="tool-card">-- <div class="card-icon"><i class="fas fa-certificate"></i></div>++ <div class="card-icon"><i class="fas fa-ranking-star"></i></div><h3>Badge & Sticker Maker</h3><p>Create rounded badges, stickers, and labels. Choose background shapes, colors, and text styles.</p><span class="card-domain">badge.openvibe.tools</span>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script><style>@@@ -49,7 +48,7 @@<body><div class="tool-wrap" style="max-width: 1100px;"><div class="page-header">-- <h1><i class="fab fa-markdown"></i> Markdown Preview</h1>++ <h1><i class="fas fa-code"></i> Markdown Preview</h1><p class="subtitle">Write Markdown on the left, see it rendered on the right. Export as HTML.</p></div>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script><style>@@@ -136,7 +135,7 @@</div><div class="swap-row">-- <button class="swap-btn" onclick="swapCurrencies()" title="Swap"><i class="fas fa-arrow-down-up-across-line"></i></button>++ <button class="swap-btn" onclick="swapCurrencies()" title="Swap"><i class="fas fa-arrows-up-down-left-right"></i></button></div><div class="currency-box" id="box-bottom">@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script><style>@@@ -115,7 +114,7 @@<body><div class="tool-wrap"><div class="page-header">-- <h1><i class="fas fa-pepper-hot"></i> Mexican Slang</h1>++ <h1><i class="fas fa-fire"></i> Mexican Slang</h1><p class="subtitle">Real Mexican Spanish — the words they don't teach you in class. ¡No mames, güey!</p></div>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head>@@@ -18,14 +18,13 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head><body><div class="tool-wrap"><div class="page-header">-- <h1><i class="fas fa-sort-alpha-down"></i> Sort & Dedupe Lines</h1>++ <h1><i class="fas fa-arrow-down-short-wide"></i> Sort & Dedupe Lines</h1><p class="subtitle">Sort lines alphabetically or reverse. Remove duplicate lines instantly.</p></div>@@@ -41,11 +40,11 @@ bananaelderberry</textarea></div><div class="btn-row">-- <button class="btn btn-primary" onclick="sortAsc()"><i class="fas fa-sort-alpha-down"></i> Sort A→Z</button>-- <button class="btn btn-secondary" onclick="sortDesc()"><i class="fas fa-sort-alpha-up"></i> Sort Z→A</button>-- <button class="btn btn-secondary" onclick="shuffle()"><i class="fas fa-shuffle"></i> Shuffle</button>++ <button class="btn btn-primary" onclick="sortAsc()"><i class="fas fa-arrow-down-short-wide"></i> Sort A→Z</button>++ <button class="btn btn-secondary" onclick="sortDesc()"><i class="fas fa-arrow-down-wide-short"></i> Sort Z→A</button>++ <button class="btn btn-secondary" onclick="shuffle()"><i class="fas fa-right-left"></i> Shuffle</button><button class="btn btn-secondary" onclick="dedupe()"><i class="fas fa-filter"></i> Remove Duplicates</button>-- <button class="btn btn-ghost" onclick="reverseLines()"><i class="fas fa-retweet"></i> Reverse Order</button>++ <button class="btn btn-ghost" onclick="reverseLines()"><i class="fas fa-arrow-right-arrow-left"></i> Reverse Order</button></div></div>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script><style>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head>@@@ -56,11 +55,11 @@const collected = document.getElementById('collected');const categoryIcons = {-- arrows: 'fa-right-long', stars: 'fa-star', hearts: 'fa-heart', music: 'fa-music',-- math: 'fa-superscript', currency: 'fa-dollar-sign', bullets: 'fa-list',-- boxes: 'fa-square', lines: 'fa-grip-lines', dividers: 'fa-minus',-- check: 'fa-check', zodiac: 'fa-star-of-david', weather: 'fa-cloud-sun',-- gaming: 'fa-chess', hands: 'fa-hand', faces: 'fa-face-smile',++ arrows: 'fa-arrow-right', stars: 'fa-star', hearts: 'fa-heart', music: 'fa-music',++ math: 'fa-arrow-up', currency: 'fa-dollar-sign', bullets: 'fa-list',++ boxes: 'fa-square', lines: 'fa-grip-vertical', dividers: 'fa-minus',++ check: 'fa-check', zodiac: 'fa-star', weather: 'fa-cloud-sun',++ gaming: 'fa-gamepad', hands: 'fa-hand', faces: 'fa-face-smile',};for (const [name, syms] of Object.entries(TextOpenVibeEngine.symbols)) {@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script><style>@@@ -73,7 +72,7 @@<h3><i class="fas fa-image"></i> Background</h3><label class="upload-btn" id="uploadBtn"><input type="file" id="fileInput" accept="image/*">-- <i class="fas fa-cloud-upload-alt"></i> Upload Background Image++ <i class="fas fa-cloud-arrow-up"></i> Upload Background Image</label><div class="ctrl-row" style="margin-top: .75rem;"><label>BG Color</label>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script><style>@@@ -83,7 +82,7 @@</div><div class="control-group">-- <h3><i class="fas fa-fill-drip"></i> Colors</h3>++ <h3><i class="fas fa-droplet"></i> Colors</h3><div class="control-row"><label>Text Color</label><input type="color" id="color" value="#ffffff">@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script><style>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script><style>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script><style>@@@ -56,7 +55,7 @@<div><div class="drop-zone" id="dropZone" onclick="document.getElementById('fileInput').click()"><input type="file" id="fileInput" accept="image/*">-- <i class="fas fa-cloud-upload-alt" style="font-size: 2rem; color: var(--text-muted); margin-bottom: .5rem; display: block;"></i>++ <i class="fas fa-cloud-arrow-up" style="font-size: 2rem; color: var(--text-muted); margin-bottom: .5rem; display: block;"></i><p style="color: var(--text-secondary); margin: 0;">Click or drag an image here</p></div><div class="wm-preview" id="previewArea" style="margin-top: 1.5rem;">@@@ -80,7 +79,7 @@</div><div class="ctrl-group">-- <h3><i class="fas fa-crosshairs"></i> Position</h3>++ <h3><i class="fas fa-location-dot"></i> Position</h3><div class="pos-grid" id="posGrid"><button class="pos-btn" data-pos="top-left">↖ Top Left</button><button class="pos-btn" data-pos="top-center">↑ Top</button>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script><style>@@@ -18,7 +18,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/shared.css"><script src="/shared/theme-loader.js" defer></script></head>@@@ -60,12 +59,12 @@<div class="btn-row"><button class="btn btn-primary" onclick="copyOutput()"><i class="fas fa-copy"></i> Copy</button><button class="btn btn-secondary" onclick="regenerate()"><i class="fas fa-rotate"></i> Regenerate</button>-- <button class="btn btn-ghost" onclick="OpenVibeUI.shareText(lastOutput)"><i class="fas fa-share"></i> Share</button>++ <button class="btn btn-ghost" onclick="OpenVibeUI.shareText(lastOutput)"><i class="fas fa-arrow-up-from-bracket"></i> Share</button></div></div><div class="tool-section">-- <h2><i class="fas fa-broom"></i> Need to clean Zalgo?</h2>++ <h2><i class="fas fa-eraser"></i> Need to clean Zalgo?</h2><p style="color: var(--text-secondary); margin-bottom: 12px;">Paste Zalgo text and strip all the combining characters.</p><div class="input-group"><textarea id="cleanInput" placeholder="Paste Zalgo text to clean..." rows="3"></textarea>@@@ -64,9 +64,9 @@ app.use(helmet({contentSecurityPolicy: {directives: {defaultSrc: ["'self'"],-- scriptSrc: ["'self'", "'unsafe-inline'", "cdnjs.cloudflare.com", "cdn.jsdelivr.net", "fonts.googleapis.com", "https://openvibe.network"],-- styleSrc: ["'self'", "'unsafe-inline'", "cdnjs.cloudflare.com", "fonts.googleapis.com", "fonts.gstatic.com"],-- fontSrc: ["'self'", "fonts.gstatic.com", "cdnjs.cloudflare.com"],++ scriptSrc: ["'self'", "'unsafe-inline'", "cdn.jsdelivr.net", "fonts.googleapis.com", "https://openvibe.network"],++ styleSrc: ["'self'", "'unsafe-inline'", "fonts.googleapis.com", "fonts.gstatic.com"],++ fontSrc: ["'self'", "fonts.gstatic.com"],imgSrc: ["'self'", "data:", "blob:"],// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).connectSrc: ["'self'", "https://openvibe.network", "https://api.exchangerate-api.com", "https://openvibe.events"],@@@ -19,7 +19,7 @@"iovalkey": "^0.4.0","jsonwebtoken": "^9.0.2","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0","pg": "^8.23.0","uuid": "^11.1.0"}@@@ -1005,9 +1005,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.56.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",-- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",++ "version": "4.73.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",++ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -17,7 +17,6 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"><link rel="stylesheet" href="/css/style.css"></head><body>@@@ -90,7 +89,7 @@<i class="fa-solid fa-download"></i> Save File</a><button class="btn btn-secondary" id="new-btn">-- <i class="fa-solid fa-arrow-rotate-left"></i> Download Another++ <i class="fa-solid fa-rotate-left"></i> Download Another</button></div></section>@@@ -99,7 +98,7 @@<section class="error-section" id="error-section" style="display:none"><div class="error-icon"><i class="fa-solid fa-circle-exclamation"></i></div><p id="error-msg"></p>-- <button class="btn btn-secondary" id="retry-btn"><i class="fa-solid fa-arrow-rotate-left"></i> Try Again</button>++ <button class="btn btn-secondary" id="retry-btn"><i class="fa-solid fa-rotate-left"></i> Try Again</button></section></div>@@@ -79,9 +79,9 @@ app.use(helmet({contentSecurityPolicy: {directives: {defaultSrc: ["'self'"],-- scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://openvibe.network", "https://static.cloudflareinsights.com"],-- styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],-- fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],++ scriptSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://openvibe.network", "https://static.cloudflareinsights.com"],++ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],++ fontSrc: ["'self'", "https://fonts.gstatic.com"],imgSrc: ["'self'", "data:", "https://i.ytimg.com", "https://img.youtube.com", "https://*.ggpht.com", "https://openvibe.network", "https://openvibe.media"],// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).connectSrc: ["'self'", "https://openvibe.network", "https://openvibe.tools", "https://*.openvibe.tools", "https://cloudflareinsights.com", "https://openvibe.events"],nnew file mode 100644@@@ -0,0 +1,83 @@++'use strict';++// No Font Awesome (plan L54 K4; the owner, 2026-11-01: "get rid of Font Awesome completely and just use our own++// openvibe.design system"). OpenVibe.Tools's icons are the OpenVibe glyphs (openvibe-shared/glyphs, drawn in the browser by++// /shared/ov-glyphs.js, which the Shared navbar loads on a page that names an icon) and the brand logos from++// openvibe-shared/brands. This holds it there:++// - no page or stylesheet links Font Awesome, and nothing draws with Font Awesome's font;++// - every page that names an icon loads the navbar or the glyph drop-in;++// - every fa- name OpenVibe.Tools uses is one the glyphs or the brands draw (a name nobody draws would show nothing).++// node test/no-font-awesome.test.js++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const ROOT = path.join(__dirname, '..');++// openvibe-shared is pinned per app here (each app has its own node_modules), so the glyphs come from one of them.++const sharedDir = fs.existsSync(path.join(ROOT, 'node_modules', 'openvibe-shared'))++ ? path.join(ROOT, 'node_modules', 'openvibe-shared')++ : path.join(ROOT, 'apps', 'gateway', 'node_modules', 'openvibe-shared');++const glyphs = require(path.join(sharedDir, 'glyphs'));++const brands = require(path.join(sharedDir, 'brands'));++++const walk = (dir, ok, out = []) => {++ if (!fs.existsSync(dir)) return out;++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {++ if (e.name === 'node_modules' || e.name === 'vendor' || e.name.startsWith('.')) continue;++ const p = path.join(dir, e.name);++ if (e.isDirectory()) walk(p, ok, out); else if (ok.test(e.name)) out.push(p);++ }++ return out;++};++const rel = (p) => path.relative(ROOT, p);++++// The pages (whole HTML files) and the code that draws them.++const PAGE_ROOTS = ["apps"];++const CODE_ROOTS = ["apps"];++const LAYOUTS = []; // the server-rendered pages: their layout brings the navbar or the glyphs++++// A Font Awesome stylesheet, wherever it is written.++const FA_CSS = /fontawesome[^"'\s]*\.css|font-awesome[^"'\s]*\.css|cdnjs\.cloudflare\.com\/ajax\/libs\/font-awesome/i;++++const pages = [];++for (const r of PAGE_ROOTS) pages.push(...walk(path.join(ROOT, r), /\.html$/));++for (const p of pages) {++ const html = fs.readFileSync(p, 'utf8');++ assert.ok(!/fontawesome|font-awesome/i.test(html), `${rel(p)} links Font Awesome`);++ // A whole page (with a <head>; fragments are parts of a page) that names an icon loads what draws it.++ if (/<head[\s>]/i.test(html) && /\bfa-[a-z]/.test(html) && !/\/shared\/navbar|ov-glyphs\.js/.test(html)) {++ assert.fail(`${rel(p)} has fa- icons but loads neither the navbar nor /shared/ov-glyphs.js`);++ }++}++++// Nothing draws with Font Awesome's font.++for (const r of CODE_ROOTS) {++ for (const p of walk(path.join(ROOT, r), /\.css$/)) {++ assert.ok(!/font-family:\s*["']?Font ?Awesome/i.test(fs.readFileSync(p, 'utf8')), `${rel(p)} draws with Font Awesome's font`);++ }++ for (const p of walk(path.join(ROOT, r), /\.(html|js|mjs|cjs)$/)) {++ const t = fs.readFileSync(p, 'utf8');++ assert.ok(!/['"]\d+px FontAwesome|Font Awesome 6 Free/.test(t), `${rel(p)} draws with Font Awesome's font`);++ assert.ok(!FA_CSS.test(t), `${rel(p)} links Font Awesome`);++ }++}++++// The server-rendered pages get the glyphs from the Shared navbar (shell.page) or from the drop-in.++for (const f of LAYOUTS) {++ const t = fs.readFileSync(path.join(ROOT, f), 'utf8');++ assert.ok(/shell\.page|ov-glyphs\.js|\/shared\/navbar/.test(t), `${f} renders pages without the navbar or /shared/ov-glyphs.js`);++}++++// Every fa- name in the pages, the scripts and the server views is drawn by a glyph or a brand.++const MOD = /^(solid|regular|light|thin|duotone|brands|sharp|classic|fw|lg|xl|xs|sm|2xs|2xl|\d+x|li|ul|border|inverse|pull-left|pull-right|rotate-\d+|rotate-by|flip-horizontal|flip-vertical|flip-both|width-auto|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|stack|stack-1x|stack-2x|layers|layers-text|layers-counter|swap-opacity|stack-text)$/;++const files = [];++for (const r of CODE_ROOTS) files.push(...walk(path.join(ROOT, r), /\.(html|js)$/));++const missing = new Map();++for (const f of files) {++ for (const m of fs.readFileSync(f, 'utf8').matchAll(/\bfa-([a-z0-9]+(?:-[a-z0-9]+)*)\b/g)) {++ const n = m[1];++ if (MOD.test(n) || glyphs.fromFa(n) || brands.fromFa(n)) continue;++ if (!missing.has(n)) missing.set(n, rel(f));++ }++}++assert.deepStrictEqual([...missing].map(([n, f]) => `fa-${n} (${f})`), [], 'every icon this site names is drawn by the OpenVibe glyphs or brands');++++console.log(`no font awesome: ${pages.length} pages, ${files.length} files, every fa- name drawn`);
@@ -22,7 +22,7 @@
@@ -22,7 +22,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"uuid": "^11.1.0"
"uuid": "^11.1.0"
}
}
@@ -1330,9 +1330,9 @@
@@ -1330,9 +1330,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.56.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
+ "version": "4.73.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
+ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -20,7 +20,6 @@
@@ -20,7 +20,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/style.css">
</head>
</head>
<body>
<body>
@@ -45,10 +44,10 @@
@@ -45,10 +44,10 @@
<button class="tool-tab" data-tool="normalize"><i class="fa-solid fa-sliders"></i> Normalize</button>
<button class="tool-tab" data-tool="normalize"><i class="fa-solid fa-sliders"></i> Normalize</button>
<button class="tool-tab" data-tool="fade"><i class="fa-solid fa-volume-low"></i> Fade</button>
<button class="tool-tab" data-tool="fade"><i class="fa-solid fa-volume-low"></i> Fade</button>
<button class="tool-tab" data-tool="bass"><i class="fa-solid fa-volume-high"></i> Bass</button>
<button class="tool-tab" data-tool="bass"><i class="fa-solid fa-volume-high"></i> Bass</button>
- <button class="tool-tab" data-tool="equalizer"><i class="fa-solid fa-bars-staggered"></i> EQ</button>
+ <button class="tool-tab" data-tool="equalizer"><i class="fa-solid fa-sliders"></i> EQ</button>
<button class="tool-tab" data-tool="voice"><i class="fa-solid fa-user-astronaut"></i> Voice FX</button>
<button class="tool-tab" data-tool="voice"><i class="fa-solid fa-user-astronaut"></i> Voice FX</button>
<button class="tool-tab" data-tool="echo"><i class="fa-solid fa-tower-broadcast"></i> Echo</button>
<button class="tool-tab" data-tool="echo"><i class="fa-solid fa-tower-broadcast"></i> Echo</button>
- <button class="tool-tab" data-tool="reverb"><i class="fa-solid fa-church"></i> Reverb</button>
+ <button class="tool-tab" data-tool="reverb"><i class="fa-solid fa-house-chimney"></i> Reverb</button>
<button class="tool-tab" data-tool="extract"><i class="fa-solid fa-music"></i> Extract</button>
<button class="tool-tab" data-tool="extract"><i class="fa-solid fa-music"></i> Extract</button>
<button class="tool-tab" data-tool="ringtone"><i class="fa-solid fa-bell"></i> Ringtone</button>
<button class="tool-tab" data-tool="ringtone"><i class="fa-solid fa-bell"></i> Ringtone</button>
</nav>
</nav>
@@ -303,7 +302,7 @@
@@ -303,7 +302,7 @@
<i class="fa-solid fa-download"></i> Download
<i class="fa-solid fa-download"></i> Download
</a>
</a>
<button class="btn btn-secondary" id="another-btn">
<button class="btn btn-secondary" id="another-btn">
- <i class="fa-solid fa-arrow-rotate-left"></i> Process Another
+ <i class="fa-solid fa-rotate-left"></i> Process Another
</button>
</button>
</div>
</div>
</section>
</section>
@@ -332,7 +331,7 @@
@@ -332,7 +331,7 @@
<span class="cross-info"><span class="cross-name">OpenVibe.Live</span><span class="cross-desc">Live streaming platform</span></span>
<span class="cross-info"><span class="cross-name">OpenVibe.Live</span><span class="cross-desc">Live streaming platform</span></span>
</a>
</a>
<a class="cross-card" href="https://openvibe.games">
<a class="cross-card" href="https://openvibe.games">
- <span class="cross-icon"><i class="fa-solid fa-dragon"></i></span>
+ <span class="cross-icon"><i class="fa-solid fa-gamepad"></i></span>
<span class="cross-info"><span class="cross-name">Scraplandia</span><span class="cross-desc">Multiplayer RPG</span></span>
<span class="cross-info"><span class="cross-name">Scraplandia</span><span class="cross-desc">Multiplayer RPG</span></span>
</a>
</a>
<a class="cross-card" href="https://maps.openvibe.tools">
<a class="cross-card" href="https://maps.openvibe.tools">
@@ -648,7 +648,7 @@
@@ -648,7 +648,7 @@
{ label: 'Format Conversion', icon: 'fa-solid fa-arrows-rotate', ids: ['mp3', 'wav', 'flac', 'ogg', 'm4a', 'aac', 'opus', 'wma', 'aiff', 'ac3'] },
{ label: 'Format Conversion', icon: 'fa-solid fa-arrows-rotate', ids: ['mp3', 'wav', 'flac', 'ogg', 'm4a', 'aac', 'opus', 'wma', 'aiff', 'ac3'] },
{ label: 'Audio Editing', icon: 'fa-solid fa-scissors', ids: ['trim', 'merge', 'fade', 'loop', 'silence', 'stereo', 'normalize', 'speed', 'reverse'] },
{ label: 'Audio Editing', icon: 'fa-solid fa-scissors', ids: ['trim', 'merge', 'fade', 'loop', 'silence', 'stereo', 'normalize', 'speed', 'reverse'] },
{ label: 'Audio Effects', icon: 'fa-solid fa-wand-magic-sparkles', ids: ['pitch', 'bass', 'equalizer', 'echo', 'reverb', 'chorus', 'distortion', 'compressor', 'bitcrusher', 'voice', 'noise'] },
{ label: 'Audio Effects', icon: 'fa-solid fa-wand-magic-sparkles', ids: ['pitch', 'bass', 'equalizer', 'echo', 'reverb', 'chorus', 'distortion', 'compressor', 'bitcrusher', 'voice', 'noise'] },
- { label: 'Extraction & Analysis', icon: 'fa-solid fa-magnifying-glass-chart', ids: ['extract', 'vocal', 'waveform', 'metadata'] },
+ { label: 'Extraction & Analysis', icon: 'fa-solid fa-magnifying-glass', ids: ['extract', 'vocal', 'waveform', 'metadata'] },
{ label: 'Specialized', icon: 'fa-solid fa-star', ids: ['ringtone', 'podcast'] },
{ label: 'Specialized', icon: 'fa-solid fa-star', ids: ['ringtone', 'podcast'] },
];
];
@@ -122,7 +122,7 @@ const DOMAIN_MAP = {
@@ -122,7 +122,7 @@ const DOMAIN_MAP = {
},
},
'loop.openvibe.tools': {
'loop.openvibe.tools': {
toolId: 'loop', brandName: 'OpenVibeLoop', defaultOp: 'loop',
toolId: 'loop', brandName: 'OpenVibeLoop', defaultOp: 'loop',
- faIcon: 'fa-repeat',
+ faIcon: 'fa-arrows-rotate',
seoTitle: 'OpenVibeLoop — Loop Audio Online',
seoTitle: 'OpenVibeLoop — Loop Audio Online',
seoDescription: 'Loop audio files a set number of times. Create repeated versions of any sound. Online audio looper.',
seoDescription: 'Loop audio files a set number of times. Create repeated versions of any sound. Online audio looper.',
},
},
@@ -134,7 +134,7 @@ const DOMAIN_MAP = {
@@ -134,7 +134,7 @@ const DOMAIN_MAP = {
},
},
'equalizer.openvibe.tools': {
'equalizer.openvibe.tools': {
toolId: 'equalizer', brandName: 'OpenVibeEQ', defaultOp: 'equalizer',
toolId: 'equalizer', brandName: 'OpenVibeEQ', defaultOp: 'equalizer',
- faIcon: 'fa-bars-staggered',
+ faIcon: 'fa-sliders',
seoTitle: 'OpenVibeEQ — Online Audio Equalizer',
seoTitle: 'OpenVibeEQ — Online Audio Equalizer',
seoDescription: 'Apply equalizer presets to audio files. Boost bass, treble, vocals and more. Online EQ tool.',
seoDescription: 'Apply equalizer presets to audio files. Boost bass, treble, vocals and more. Online EQ tool.',
},
},
@@ -174,7 +174,7 @@ const DOMAIN_MAP = {
@@ -174,7 +174,7 @@ const DOMAIN_MAP = {
},
},
'podcast.openvibe.tools': {
'podcast.openvibe.tools': {
toolId: 'podcast', brandName: 'OpenVibePodcast', defaultOp: 'podcast',
toolId: 'podcast', brandName: 'OpenVibePodcast', defaultOp: 'podcast',
- faIcon: 'fa-podcast',
+ faIcon: 'fa-microphone',
seoTitle: 'OpenVibePodcast — Optimize Audio for Podcasts Online',
seoTitle: 'OpenVibePodcast — Optimize Audio for Podcasts Online',
seoDescription: 'Optimize audio for podcast publishing. Normalize loudness, compress dynamics, convert to podcast-ready format.',
seoDescription: 'Optimize audio for podcast publishing. Normalize loudness, compress dynamics, convert to podcast-ready format.',
},
},
@@ -186,7 +186,7 @@ const DOMAIN_MAP = {
@@ -186,7 +186,7 @@ const DOMAIN_MAP = {
},
},
'noise.openvibe.tools': {
'noise.openvibe.tools': {
toolId: 'noise', brandName: 'OpenVibeNoise', defaultOp: 'noise',
toolId: 'noise', brandName: 'OpenVibeNoise', defaultOp: 'noise',
- faIcon: 'fa-broom',
+ faIcon: 'fa-eraser',
seoTitle: 'OpenVibeNoise — Reduce Background Noise Online',
seoTitle: 'OpenVibeNoise — Reduce Background Noise Online',
seoDescription: 'Remove background noise from audio recordings. Clean up interviews, podcasts, and voice memos. Online tool.',
seoDescription: 'Remove background noise from audio recordings. Clean up interviews, podcasts, and voice memos. Online tool.',
},
},
@@ -204,7 +204,7 @@ const DOMAIN_MAP = {
@@ -204,7 +204,7 @@ const DOMAIN_MAP = {
},
},
'reverb.openvibe.tools': {
'reverb.openvibe.tools': {
toolId: 'reverb', brandName: 'OpenVibeReverb', defaultOp: 'reverb',
toolId: 'reverb', brandName: 'OpenVibeReverb', defaultOp: 'reverb',
- faIcon: 'fa-church',
+ faIcon: 'fa-house-chimney',
seoTitle: 'OpenVibeReverb — Add Reverb to Audio Online',
seoTitle: 'OpenVibeReverb — Add Reverb to Audio Online',
seoDescription: 'Add room reverb, hall, cathedral, and plate reverb effects to audio. Online reverb tool.',
seoDescription: 'Add room reverb, hall, cathedral, and plate reverb effects to audio. Online reverb tool.',
},
},
@@ -95,9 +95,9 @@ app.use(helmet({
@@ -95,9 +95,9 @@ app.use(helmet({
contentSecurityPolicy: {
contentSecurityPolicy: {
directives: {
directives: {
defaultSrc: ["'self'"],
defaultSrc: ["'self'"],
- scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://openvibe.network"],
- styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
- fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://openvibe.network"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "blob:", jobsRuntime.mediaOrigin()], // job result previews are 302s to Media
imgSrc: ["'self'", "data:", "blob:", jobsRuntime.mediaOrigin()], // job result previews are 302s to Media
mediaSrc: ["'self'", "blob:", jobsRuntime.mediaOrigin()],
mediaSrc: ["'self'", "blob:", jobsRuntime.mediaOrigin()],
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
@@ -117,7 +117,7 @@ const TOOLS = {
@@ -117,7 +117,7 @@ const TOOLS = {
id: 'loop',
id: 'loop',
label: 'Loop',
label: 'Loop',
description: 'Loop audio multiple times',
description: 'Loop audio multiple times',
- faIcon: 'fa-repeat',
+ faIcon: 'fa-arrows-rotate',
accepts: AUDIO_FORMATS,
accepts: AUDIO_FORMATS,
category: 'editing',
category: 'editing',
handler: loop,
handler: loop,
@@ -135,7 +135,7 @@ const TOOLS = {
@@ -135,7 +135,7 @@ const TOOLS = {
id: 'equalizer',
id: 'equalizer',
label: 'Equalizer',
label: 'Equalizer',
description: 'Apply EQ presets',
description: 'Apply EQ presets',
- faIcon: 'fa-bars-staggered',
+ faIcon: 'fa-sliders',
accepts: AUDIO_FORMATS,
accepts: AUDIO_FORMATS,
category: 'effects',
category: 'effects',
handler: equalizer,
handler: equalizer,
@@ -180,7 +180,7 @@ const TOOLS = {
@@ -180,7 +180,7 @@ const TOOLS = {
id: 'podcast',
id: 'podcast',
label: 'Podcast',
label: 'Podcast',
description: 'Optimize for podcast publishing',
description: 'Optimize for podcast publishing',
- faIcon: 'fa-podcast',
+ faIcon: 'fa-microphone',
accepts: AUDIO_FORMATS,
accepts: AUDIO_FORMATS,
category: 'specialized',
category: 'specialized',
handler: podcast,
handler: podcast,
@@ -198,7 +198,7 @@ const TOOLS = {
@@ -198,7 +198,7 @@ const TOOLS = {
id: 'noise',
id: 'noise',
label: 'Noise Reduce',
label: 'Noise Reduce',
description: 'Reduce background noise',
description: 'Reduce background noise',
- faIcon: 'fa-broom',
+ faIcon: 'fa-eraser',
accepts: AUDIO_FORMATS,
accepts: AUDIO_FORMATS,
category: 'editing',
category: 'editing',
handler: noise,
handler: noise,
@@ -225,7 +225,7 @@ const TOOLS = {
@@ -225,7 +225,7 @@ const TOOLS = {
id: 'reverb',
id: 'reverb',
label: 'Reverb',
label: 'Reverb',
description: 'Add reverb effect',
description: 'Add reverb effect',
- faIcon: 'fa-church',
+ faIcon: 'fa-house-chimney',
accepts: AUDIO_FORMATS,
accepts: AUDIO_FORMATS,
category: 'effects',
category: 'effects',
handler: reverb,
handler: reverb,
@@ -21,7 +21,7 @@
@@ -21,7 +21,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pdf-lib": "^1.17.1",
"pdf-lib": "^1.17.1",
"pg": "^8.23.0",
"pg": "^8.23.0",
"sharp": "^0.33.5",
"sharp": "^0.33.5",
@@ -1758,9 +1758,9 @@
@@ -1758,9 +1758,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.56.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
+ "version": "4.73.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
+ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -20,7 +20,6 @@
@@ -20,7 +20,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/style.css">
</head>
</head>
<body>
<body>
@@ -40,8 +39,8 @@
@@ -40,8 +39,8 @@
<button class="tool-tab" data-tool="split"><i class="fa-solid fa-scissors"></i> Split</button>
<button class="tool-tab" data-tool="split"><i class="fa-solid fa-scissors"></i> Split</button>
<button class="tool-tab" data-tool="compress"><i class="fa-solid fa-compress"></i> Compress</button>
<button class="tool-tab" data-tool="compress"><i class="fa-solid fa-compress"></i> Compress</button>
<button class="tool-tab" data-tool="rotate"><i class="fa-solid fa-rotate"></i> Rotate</button>
<button class="tool-tab" data-tool="rotate"><i class="fa-solid fa-rotate"></i> Rotate</button>
- <button class="tool-tab" data-tool="reorder"><i class="fa-solid fa-sort"></i> Reorder</button>
- <button class="tool-tab" data-tool="watermark"><i class="fa-solid fa-stamp"></i> Watermark</button>
+ <button class="tool-tab" data-tool="reorder"><i class="fa-solid fa-arrow-down-wide-short"></i> Reorder</button>
+ <button class="tool-tab" data-tool="watermark"><i class="fa-solid fa-signature"></i> Watermark</button>
<button class="tool-tab" data-tool="img2pdf"><i class="fa-solid fa-file-image"></i> IMG→PDF</button>
<button class="tool-tab" data-tool="img2pdf"><i class="fa-solid fa-file-image"></i> IMG→PDF</button>
<button class="tool-tab" data-tool="pdf2img"><i class="fa-solid fa-image"></i> PDF→IMG</button>
<button class="tool-tab" data-tool="pdf2img"><i class="fa-solid fa-image"></i> PDF→IMG</button>
</nav>
</nav>
@@ -208,7 +207,7 @@
@@ -208,7 +207,7 @@
<i class="fa-solid fa-download"></i> Download
<i class="fa-solid fa-download"></i> Download
</a>
</a>
<button class="btn btn-secondary" id="another-btn">
<button class="btn btn-secondary" id="another-btn">
- <i class="fa-solid fa-arrow-rotate-left"></i> Process Another
+ <i class="fa-solid fa-rotate-left"></i> Process Another
</button>
</button>
</div>
</div>
</section>
</section>
@@ -671,10 +671,10 @@
@@ -671,10 +671,10 @@
{ host: 'splitpdf.openvibe.tools', name: 'SplitPDF', icon: 'fa-scissors', desc: 'Split by page range' },
{ host: 'splitpdf.openvibe.tools', name: 'SplitPDF', icon: 'fa-scissors', desc: 'Split by page range' },
{ host: 'compresspdf.openvibe.tools', name: 'CompressPDF', icon: 'fa-compress', desc: 'Reduce file size' },
{ host: 'compresspdf.openvibe.tools', name: 'CompressPDF', icon: 'fa-compress', desc: 'Reduce file size' },
{ host: 'rotatepdf.openvibe.tools', name: 'RotatePDF', icon: 'fa-rotate', desc: 'Rotate PDF pages' },
{ host: 'rotatepdf.openvibe.tools', name: 'RotatePDF', icon: 'fa-rotate', desc: 'Rotate PDF pages' },
- { host: 'reorderpdf.openvibe.tools', name: 'ReorderPDF', icon: 'fa-sort', desc: 'Rearrange pages' },
- { host: 'watermarkpdf.openvibe.tools', name: 'WatermarkPDF', icon: 'fa-stamp', desc: 'Add watermarks' },
+ { host: 'reorderpdf.openvibe.tools', name: 'ReorderPDF', icon: 'fa-arrow-down-wide-short', desc: 'Rearrange pages' },
+ { host: 'watermarkpdf.openvibe.tools', name: 'WatermarkPDF', icon: 'fa-signature', desc: 'Add watermarks' },
{ host: 'protectpdf.openvibe.tools', name: 'ProtectPDF', icon: 'fa-lock', desc: 'Password protect' },
{ host: 'protectpdf.openvibe.tools', name: 'ProtectPDF', icon: 'fa-lock', desc: 'Password protect' },
- { host: 'unlockpdf.openvibe.tools', name: 'UnlockPDF', icon: 'fa-lock-open', desc: 'Remove password' },
+ { host: 'unlockpdf.openvibe.tools', name: 'UnlockPDF', icon: 'fa-unlock', desc: 'Remove password' },
{ host: 'image2pdf.openvibe.tools', name: 'Image2PDF', icon: 'fa-file-image', desc: 'Images to PDF' },
{ host: 'image2pdf.openvibe.tools', name: 'Image2PDF', icon: 'fa-file-image', desc: 'Images to PDF' },
{ host: 'jpg2pdf.openvibe.tools', name: 'JPG2PDF', icon: 'fa-file-image', desc: 'JPG to PDF' },
{ host: 'jpg2pdf.openvibe.tools', name: 'JPG2PDF', icon: 'fa-file-image', desc: 'JPG to PDF' },
{ host: 'png2pdf.openvibe.tools', name: 'PNG2PDF', icon: 'fa-file-image', desc: 'PNG to PDF' },
{ host: 'png2pdf.openvibe.tools', name: 'PNG2PDF', icon: 'fa-file-image', desc: 'PNG to PDF' },
@@ -50,13 +50,13 @@ const DOMAIN_MAP = {
@@ -50,13 +50,13 @@ const DOMAIN_MAP = {
},
},
'reorderpdf.openvibe.tools': {
'reorderpdf.openvibe.tools': {
toolId: 'reorder', brandName: 'ReorderPDF', defaultOp: 'reorder',
toolId: 'reorder', brandName: 'ReorderPDF', defaultOp: 'reorder',
- faIcon: 'fa-sort',
+ faIcon: 'fa-arrow-down-wide-short',
seoTitle: 'ReorderPDF — Rearrange PDF Pages Online',
seoTitle: 'ReorderPDF — Rearrange PDF Pages Online',
seoDescription: 'Drag and drop to reorder pages in your PDF. Remove unwanted pages. Online PDF page organizer.',
seoDescription: 'Drag and drop to reorder pages in your PDF. Remove unwanted pages. Online PDF page organizer.',
},
},
'watermarkpdf.openvibe.tools': {
'watermarkpdf.openvibe.tools': {
toolId: 'watermark', brandName: 'WatermarkPDF', defaultOp: 'watermark',
toolId: 'watermark', brandName: 'WatermarkPDF', defaultOp: 'watermark',
- faIcon: 'fa-stamp',
+ faIcon: 'fa-signature',
seoTitle: 'WatermarkPDF — Add Watermarks to PDFs Online',
seoTitle: 'WatermarkPDF — Add Watermarks to PDFs Online',
seoDescription: 'Add text or image watermarks to PDF documents. Customize position, opacity, rotation, and font. Online.',
seoDescription: 'Add text or image watermarks to PDF documents. Customize position, opacity, rotation, and font. Online.',
},
},
@@ -68,7 +68,7 @@ const DOMAIN_MAP = {
@@ -68,7 +68,7 @@ const DOMAIN_MAP = {
},
},
'unlockpdf.openvibe.tools': {
'unlockpdf.openvibe.tools': {
toolId: 'unlock', brandName: 'UnlockPDF', defaultOp: 'unlock',
toolId: 'unlock', brandName: 'UnlockPDF', defaultOp: 'unlock',
- faIcon: 'fa-lock-open',
+ faIcon: 'fa-unlock',
seoTitle: 'UnlockPDF — Remove PDF Password Online',
seoTitle: 'UnlockPDF — Remove PDF Password Online',
seoDescription: 'Remove password protection from PDFs. Unlock PDF files for editing and printing. Online.',
seoDescription: 'Remove password protection from PDFs. Unlock PDF files for editing and printing. Online.',
},
},
@@ -112,9 +112,9 @@ app.use(helmet({
@@ -112,9 +112,9 @@ app.use(helmet({
contentSecurityPolicy: {
contentSecurityPolicy: {
directives: {
directives: {
defaultSrc: ["'self'"],
defaultSrc: ["'self'"],
- scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://openvibe.network"],
- styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
- fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://openvibe.network"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "blob:"],
imgSrc: ["'self'", "data:", "blob:"],
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
connectSrc: ["'self'", "https://openvibe.network", "https://*.openvibe.tools", "https://openvibe.events"],
connectSrc: ["'self'", "https://openvibe.network", "https://*.openvibe.tools", "https://openvibe.events"],
@@ -60,7 +60,7 @@ const TOOLS = {
@@ -60,7 +60,7 @@ const TOOLS = {
id: 'reorder',
id: 'reorder',
label: 'Reorder Pages',
label: 'Reorder Pages',
description: 'Rearrange or remove pages in a PDF',
description: 'Rearrange or remove pages in a PDF',
- faIcon: 'fa-sort',
+ faIcon: 'fa-arrow-down-wide-short',
accepts: ['pdf'],
accepts: ['pdf'],
multiFile: false,
multiFile: false,
handler: reorder,
handler: reorder,
@@ -69,7 +69,7 @@ const TOOLS = {
@@ -69,7 +69,7 @@ const TOOLS = {
id: 'watermark',
id: 'watermark',
label: 'Watermark',
label: 'Watermark',
description: 'Add text watermarks to PDF pages',
description: 'Add text watermarks to PDF pages',
- faIcon: 'fa-stamp',
+ faIcon: 'fa-signature',
accepts: ['pdf'],
accepts: ['pdf'],
multiFile: false,
multiFile: false,
handler: watermark,
handler: watermark,
@@ -88,7 +88,7 @@ const TOOLS = {
@@ -88,7 +88,7 @@ const TOOLS = {
id: 'unlock',
id: 'unlock',
label: 'Unlock PDF',
label: 'Unlock PDF',
description: 'Remove password from a PDF',
description: 'Remove password from a PDF',
- faIcon: 'fa-lock-open',
+ faIcon: 'fa-unlock',
accepts: ['pdf'],
accepts: ['pdf'],
multiFile: false,
multiFile: false,
requires: [qpdf], // decryption
requires: [qpdf], // decryption
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"helmet": "^7.1.0",
"helmet": "^7.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
}
}
},
},
@@ -1129,9 +1129,9 @@
@@ -1129,9 +1129,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.56.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
+ "version": "4.73.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
+ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -32,7 +32,6 @@
@@ -32,7 +32,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css">
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.css">
@@ -45,7 +44,7 @@
@@ -45,7 +44,7 @@
<div class="loc-search">
<div class="loc-search">
<i class="fa-solid fa-location-dot"></i>
<i class="fa-solid fa-location-dot"></i>
<input type="text" id="loc-input" placeholder="Enter your city or zip code...">
<input type="text" id="loc-input" placeholder="Enter your city or zip code...">
- <button id="btn-gps" title="Use GPS"><i class="fa-solid fa-crosshairs"></i></button>
+ <button id="btn-gps" title="Use GPS"><i class="fa-solid fa-location-dot"></i></button>
<button id="btn-loc-search" class="btn-primary"><i class="fa-solid fa-magnifying-glass"></i> Find Food</button>
<button id="btn-loc-search" class="btn-primary"><i class="fa-solid fa-magnifying-glass"></i> Find Food</button>
</div>
</div>
</div>
</div>
@@ -57,7 +56,7 @@
@@ -57,7 +56,7 @@
<button class="tab active" data-tab="food-banks"><i class="fa-solid fa-hand-holding-heart"></i> Food Banks</button>
<button class="tab active" data-tab="food-banks"><i class="fa-solid fa-hand-holding-heart"></i> Food Banks</button>
<button class="tab" data-tab="stores"><i class="fa-solid fa-cart-shopping"></i> Stores</button>
<button class="tab" data-tab="stores"><i class="fa-solid fa-cart-shopping"></i> Stores</button>
<button class="tab" data-tab="meal-plan"><i class="fa-solid fa-calendar-days"></i> Meal Planner</button>
<button class="tab" data-tab="meal-plan"><i class="fa-solid fa-calendar-days"></i> Meal Planner</button>
- <button class="tab" data-tab="food-db"><i class="fa-solid fa-apple-whole"></i> Food Database</button>
+ <button class="tab" data-tab="food-db"><i class="fa-solid fa-utensils"></i> Food Database</button>
<button class="tab" data-tab="map"><i class="fa-solid fa-map"></i> Map</button>
<button class="tab" data-tab="map"><i class="fa-solid fa-map"></i> Map</button>
</nav>
</nav>
@@ -117,7 +116,7 @@
@@ -117,7 +116,7 @@
<input type="number" id="mp-days" value="3" min="1" max="7">
<input type="number" id="mp-days" value="3" min="1" max="7">
</div>
</div>
<div class="control-group">
<div class="control-group">
- <label><i class="fa-solid fa-toggle-on"></i> Options</label>
+ <label><i class="fa-solid fa-circle-check"></i> Options</label>
<div class="toggle-row">
<div class="toggle-row">
<label class="toggle"><input type="checkbox" id="mp-camp" checked> <span>Camp-friendly</span></label>
<label class="toggle"><input type="checkbox" id="mp-camp" checked> <span>Camp-friendly</span></label>
<label class="toggle"><input type="checkbox" id="mp-shelf"> <span>Shelf-stable only</span></label>
<label class="toggle"><input type="checkbox" id="mp-shelf"> <span>Shelf-stable only</span></label>
@@ -131,7 +130,7 @@
@@ -131,7 +130,7 @@
<!-- Food Database Panel -->
<!-- Food Database Panel -->
<section id="food-db" class="panel">
<section id="food-db" class="panel">
<div class="panel-header">
<div class="panel-header">
- <h2><i class="fa-solid fa-apple-whole"></i> Budget Food Database</h2>
+ <h2><i class="fa-solid fa-utensils"></i> Budget Food Database</h2>
<p class="panel-desc">Browse 80+ budget items with prices from 5 stores, nutrition info, and camp-friendliness ratings</p>
<p class="panel-desc">Browse 80+ budget items with prices from 5 stores, nutrition info, and camp-friendliness ratings</p>
</div>
</div>
<div class="db-controls">
<div class="db-controls">
@@ -141,12 +140,12 @@
@@ -141,12 +140,12 @@
</div>
</div>
<div class="db-filters">
<div class="db-filters">
<button class="chip active" data-group="all">All</button>
<button class="chip active" data-group="all">All</button>
- <button class="chip" data-group="protein"><i class="fa-solid fa-drumstick-bite"></i> Protein</button>
- <button class="chip" data-group="grains"><i class="fa-solid fa-wheat-awn"></i> Grains</button>
- <button class="chip" data-group="vegetables"><i class="fa-solid fa-carrot"></i> Veggies</button>
- <button class="chip" data-group="fruits"><i class="fa-solid fa-apple-whole"></i> Fruits</button>
- <button class="chip" data-group="dairy"><i class="fa-solid fa-glass-water"></i> Dairy</button>
- <button class="chip" data-group="fats"><i class="fa-solid fa-oil-can"></i> Fats</button>
+ <button class="chip" data-group="protein"><i class="fa-solid fa-fish"></i> Protein</button>
+ <button class="chip" data-group="grains"><i class="fa-solid fa-seedling"></i> Grains</button>
+ <button class="chip" data-group="vegetables"><i class="fa-solid fa-leaf"></i> Veggies</button>
+ <button class="chip" data-group="fruits"><i class="fa-solid fa-utensils"></i> Fruits</button>
+ <button class="chip" data-group="dairy"><i class="fa-solid fa-mug-saucer"></i> Dairy</button>
+ <button class="chip" data-group="fats"><i class="fa-solid fa-droplet"></i> Fats</button>
</div>
</div>
<div class="db-toggles">
<div class="db-toggles">
<label class="toggle"><input type="checkbox" id="db-camp"> <span>Camp-friendly</span></label>
<label class="toggle"><input type="checkbox" id="db-camp"> <span>Camp-friendly</span></label>
@@ -71,7 +71,7 @@
@@ -71,7 +71,7 @@
</div>
</div>
${b.description ? `<div class="card-desc">${esc(b.description)}</div>` : ''}
${b.description ? `<div class="card-desc">${esc(b.description)}</div>` : ''}
<div class="card-actions">
<div class="card-actions">
- ${b.lat && b.lon ? `<a class="card-btn" href="https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(b.lat)},${encodeURIComponent(b.lon)}" target="_blank"><i class="fa-solid fa-diamond-turn-right"></i> Directions</a>` : ''}
+ ${b.lat && b.lon ? `<a class="card-btn" href="https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(b.lat)},${encodeURIComponent(b.lon)}" target="_blank"><i class="fa-solid fa-route"></i> Directions</a>` : ''}
${site ? `<a class="card-btn" href="${esc(site)}" target="_blank" rel="noopener noreferrer"><i class="fa-solid fa-globe"></i> Website</a>` : ''}
${site ? `<a class="card-btn" href="${esc(site)}" target="_blank" rel="noopener noreferrer"><i class="fa-solid fa-globe"></i> Website</a>` : ''}
</div>
</div>
</div>`; }).join('');
</div>`; }).join('');
@@ -101,7 +101,7 @@
@@ -101,7 +101,7 @@
${s.phone ? `<span><i class="fa-solid fa-phone"></i> ${esc(s.phone)}</span>` : ''}
${s.phone ? `<span><i class="fa-solid fa-phone"></i> ${esc(s.phone)}</span>` : ''}
</div>
</div>
<div class="card-actions">
<div class="card-actions">
- ${s.lat && s.lon ? `<a class="card-btn" href="https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(s.lat)},${encodeURIComponent(s.lon)}" target="_blank"><i class="fa-solid fa-diamond-turn-right"></i> Directions</a>` : ''}
+ ${s.lat && s.lon ? `<a class="card-btn" href="https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(s.lat)},${encodeURIComponent(s.lon)}" target="_blank"><i class="fa-solid fa-route"></i> Directions</a>` : ''}
</div>
</div>
</div>`;
</div>`;
}).join('');
}).join('');
@@ -85,9 +85,9 @@ app.use(helmet({
@@ -85,9 +85,9 @@ app.use(helmet({
contentSecurityPolicy: {
contentSecurityPolicy: {
directives: {
directives: {
defaultSrc: ["'self'"],
defaultSrc: ["'self'"],
- scriptSrc: ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net', 'unpkg.com', 'cdnjs.cloudflare.com', 'fonts.googleapis.com', 'https://openvibe.network'],
- styleSrc: ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net', 'unpkg.com', 'cdnjs.cloudflare.com', 'fonts.googleapis.com', 'fonts.gstatic.com'],
- fontSrc: ["'self'", 'fonts.gstatic.com', 'cdnjs.cloudflare.com'],
+ scriptSrc: ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net', 'unpkg.com', 'fonts.googleapis.com', 'https://openvibe.network'],
+ styleSrc: ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net', 'unpkg.com', 'fonts.googleapis.com', 'fonts.gstatic.com'],
+ fontSrc: ["'self'", 'fonts.gstatic.com'],
imgSrc: ["'self'", 'data:', 'image.tmdb.org', '*.tile.openstreetmap.org', '*.basemaps.cartocdn.com', 'server.arcgisonline.com'],
imgSrc: ["'self'", 'data:', 'image.tmdb.org', '*.tile.openstreetmap.org', '*.basemaps.cartocdn.com', 'server.arcgisonline.com'],
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
connectSrc: ["'self'", 'nominatim.openstreetmap.org', 'https://openvibe.network', 'https://openvibe.events'],
connectSrc: ["'self'", 'nominatim.openstreetmap.org', 'https://openvibe.network', 'https://openvibe.events'],
@@ -21,7 +21,7 @@
@@ -21,7 +21,7 @@
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"zxing-wasm": "3.1.5"
"zxing-wasm": "3.1.5"
}
}
@@ -1229,9 +1229,9 @@
@@ -1229,9 +1229,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.56.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
+ "version": "4.73.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
+ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -25,7 +25,6 @@
@@ -25,7 +25,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
:root {
:root {
@@ -141,10 +140,10 @@
@@ -141,10 +140,10 @@
const AUDIO_TOOLS = {
const AUDIO_TOOLS = {
'convert': { name: 'Converter', domain: 'convert.audio.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },
'convert': { name: 'Converter', domain: 'convert.audio.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },
'trim': { name: 'Trim', domain: 'trim.openvibe.tools', icon: 'fa-scissors', category: 'Edit' },
'trim': { name: 'Trim', domain: 'trim.openvibe.tools', icon: 'fa-scissors', category: 'Edit' },
- 'merge': { name: 'Merge', domain: 'merge.openvibe.tools', icon: 'fa-code-merge', category: 'Mix' },
+ 'merge': { name: 'Merge', domain: 'merge.openvibe.tools', icon: 'fa-code-fork', category: 'Mix' },
'pitch': { name: 'Pitch', domain: 'pitch.openvibe.tools', icon: 'fa-wave-square', category: 'Effects' },
'pitch': { name: 'Pitch', domain: 'pitch.openvibe.tools', icon: 'fa-wave-square', category: 'Effects' },
'speed': { name: 'Speed', domain: 'speed.openvibe.tools', icon: 'fa-gauge-high', category: 'Edit' },
'speed': { name: 'Speed', domain: 'speed.openvibe.tools', icon: 'fa-gauge-high', category: 'Edit' },
- 'reverb': { name: 'Reverb', domain: 'reverb.openvibe.tools', icon: 'fa-church', category: 'Effects' },
+ 'reverb': { name: 'Reverb', domain: 'reverb.openvibe.tools', icon: 'fa-house-chimney', category: 'Effects' },
'eq': { name: 'Equalizer', domain: 'equalizer.openvibe.tools', icon: 'fa-sliders', category: 'Effects' },
'eq': { name: 'Equalizer', domain: 'equalizer.openvibe.tools', icon: 'fa-sliders', category: 'Effects' },
'vocal': { name: 'Vocal Extract', domain: 'vocal.openvibe.tools', icon: 'fa-microphone-lines', category: 'Extract' },
'vocal': { name: 'Vocal Extract', domain: 'vocal.openvibe.tools', icon: 'fa-microphone-lines', category: 'Extract' },
};
};
@@ -25,7 +25,6 @@
@@ -25,7 +25,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
-<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
<style>
:root{--bg:var(--bg-primary,#0b0c12);--bg2:var(--bg-secondary,#12131c);--bg3:var(--bg-tertiary,#1a1b28);--card:var(--bg-card,rgba(22,24,36,.85));--tx:var(--text-primary,#f1f4fb);--tx2:var(--text-secondary,#a0a8c4);--tx3:var(--text-muted,#5e6580);--ac:var(--accent,#a78bfa);--acl:var(--accent-light,#c4b5fd);--brd:var(--border,rgba(255,255,255,.07));--r:16px}
:root{--bg:var(--bg-primary,#0b0c12);--bg2:var(--bg-secondary,#12131c);--bg3:var(--bg-tertiary,#1a1b28);--card:var(--bg-card,rgba(22,24,36,.85));--tx:var(--text-primary,#f1f4fb);--tx2:var(--text-secondary,#a0a8c4);--tx3:var(--text-muted,#5e6580);--ac:var(--accent,#a78bfa);--acl:var(--accent-light,#c4b5fd);--brd:var(--border,rgba(255,255,255,.07));--r:16px}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
@@ -178,12 +177,12 @@ html:{n:'HTML Formatter',i:'fa-file-code',c:'data',d:'HTML formatter, minifier &
@@ -178,12 +177,12 @@ html:{n:'HTML Formatter',i:'fa-file-code',c:'data',d:'HTML formatter, minifier &
base64:{n:'Base64 Encoder & Decoder',i:'fa-lock',c:'enc',d:'Base64 encode & decode',p:'Hello, World!',m:['Encode','Decode']},
base64:{n:'Base64 Encoder & Decoder',i:'fa-lock',c:'enc',d:'Base64 encode & decode',p:'Hello, World!',m:['Encode','Decode']},
url:{n:'URL Encoder & Decoder',i:'fa-link',c:'enc',d:'URL encode, decode & parser',p:'https://openvibe.tools/search?q=hello world&lang=en#results',m:['Encode','Decode','Parse']},
url:{n:'URL Encoder & Decoder',i:'fa-link',c:'enc',d:'URL encode, decode & parser',p:'https://openvibe.tools/search?q=hello world&lang=en#results',m:['Encode','Decode','Parse']},
jwt:{n:'JWT Decoder',i:'fa-key',c:'enc',d:'JWT decoder \u2014 header, payload & expiry',p:'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhvYm8iLCJpYXQiOjE1MTYyMzkwMjJ9.4S5J5Z5mSp5F8Kv3K5qFJpQ5Y5jF5p5t5m5k5e5h5d4',m:['Decode']},
jwt:{n:'JWT Decoder',i:'fa-key',c:'enc',d:'JWT decoder \u2014 header, payload & expiry',p:'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhvYm8iLCJpYXQiOjE1MTYyMzkwMjJ9.4S5J5Z5mSp5F8Kv3K5qFJpQ5Y5jF5p5t5m5k5e5h5d4',m:['Decode']},
-uuid:{n:'UUID Generator',i:'fa-fingerprint',c:'enc',d:'UUID v4 generator & validator',p:'',m:['Generate','Bulk 10','Bulk 100','Validate']},
+uuid:{n:'UUID Generator',i:'fa-id-card',c:'enc',d:'UUID v4 generator & validator',p:'',m:['Generate','Bulk 10','Bulk 100','Validate']},
hash:{n:'Hash Generator',i:'fa-hashtag',c:'enc',d:'SHA-256, SHA-1 & SHA-512 hash generator',p:'Hello, World!',m:['SHA-256','SHA-1','SHA-512']},
hash:{n:'Hash Generator',i:'fa-hashtag',c:'enc',d:'SHA-256, SHA-1 & SHA-512 hash generator',p:'Hello, World!',m:['SHA-256','SHA-1','SHA-512']},
-hex:{n:'Hex Converter',i:'fa-barcode',c:'enc',d:'Hex encoder / decoder',p:'Hello',m:['To Hex','From Hex']},
+hex:{n:'Hex Converter',i:'fa-code',c:'enc',d:'Hex encoder / decoder',p:'Hello',m:['To Hex','From Hex']},
entities:{n:'String Escape Tool',i:'fa-shield-halved',c:'enc',d:'HTML, JS & URL escape / unescape',p:'<script>alert("hello")<\/script>',m:['HTML Enc','HTML Dec','JS Esc','JS Unesc','URL Enc','URL Dec']},
entities:{n:'String Escape Tool',i:'fa-shield-halved',c:'enc',d:'HTML, JS & URL escape / unescape',p:'<script>alert("hello")<\/script>',m:['HTML Enc','HTML Dec','JS Esc','JS Unesc','URL Enc','URL Dec']},
timestamp:{n:'Unix Timestamp Converter',i:'fa-clock',c:'time',d:'Unix timestamp \u2194 date converter',p:'',m:['Now','To Date','To Unix']},
timestamp:{n:'Unix Timestamp Converter',i:'fa-clock',c:'time',d:'Unix timestamp \u2194 date converter',p:'',m:['Now','To Date','To Unix']},
-cron:{n:'Cron Expression Parser',i:'fa-calendar-check',c:'time',d:'Cron expression parser & next runs',p:'*/15 9-17 * * 1-5',m:['Parse']},
+cron:{n:'Cron Expression Parser',i:'fa-calendar-days',c:'time',d:'Cron expression parser & next runs',p:'*/15 9-17 * * 1-5',m:['Parse']},
beautify:{n:'Code Beautifier',i:'fa-wand-magic-sparkles',c:'qual',d:'Beautify JS, CSS, HTML & JSON',p:'{"a":1,"b":[2,3],"c":{"d":true}}',m:['JSON','HTML','CSS']},
beautify:{n:'Code Beautifier',i:'fa-wand-magic-sparkles',c:'qual',d:'Beautify JS, CSS, HTML & JSON',p:'{"a":1,"b":[2,3],"c":{"d":true}}',m:['JSON','HTML','CSS']},
jsminify:{n:"JavaScript Minifier",i:'fa-compress',c:'qual',d:"Shrink JavaScript with a real parser, not regex",p:"function greet(name) {\n // say hello\n const message = \"Hello, \" + name + \"!\";\n console.log(message);\n return message;\n}\ngreet(\"OpenVibe\");",m:["Minify", "Minify + mangle", "Whitespace only"]},
jsminify:{n:"JavaScript Minifier",i:'fa-compress',c:'qual',d:"Shrink JavaScript with a real parser, not regex",p:"function greet(name) {\n // say hello\n const message = \"Hello, \" + name + \"!\";\n console.log(message);\n return message;\n}\ngreet(\"OpenVibe\");",m:["Minify", "Minify + mangle", "Whitespace only"]},
jsformat:{n:"JavaScript Beautifier",i:'fa-wand-magic-sparkles',c:'qual',d:"Turn minified or messy JavaScript into readable code",p:"function greet(n){const m=\"Hello, \"+n+\"!\";console.log(m);return m}greet(\"OpenVibe\");",m:["Beautify"]},
jsformat:{n:"JavaScript Beautifier",i:'fa-wand-magic-sparkles',c:'qual',d:"Turn minified or messy JavaScript into readable code",p:"function greet(n){const m=\"Hello, \"+n+\"!\";console.log(m);return m}greet(\"OpenVibe\");",m:["Beautify"]},
@@ -198,10 +197,10 @@ jsonstringify:{n:"JSON Stringify",i:'fa-quote-right',c:'data',d:"Turn JSON into
@@ -198,10 +197,10 @@ jsonstringify:{n:"JSON Stringify",i:'fa-quote-right',c:'data',d:"Turn JSON into
html2md:{n:"HTML to Markdown",i:'fa-right-left',c:'data',d:"Convert HTML into clean Markdown",p:"\u003ch1>Hello World\u003c/h1>\n<p>This is <strong>bold</strong>, <em>italic</em> and a <a href=\"https://openvibe.network\">link</a>.</p>\n<ul><li>Item one</li><li>Item two</li></ul>\n<pre><code>code block</code></pre>",m:["Convert"]},
html2md:{n:"HTML to Markdown",i:'fa-right-left',c:'data',d:"Convert HTML into clean Markdown",p:"\u003ch1>Hello World\u003c/h1>\n<p>This is <strong>bold</strong>, <em>italic</em> and a <a href=\"https://openvibe.network\">link</a>.</p>\n<ul><li>Item one</li><li>Item two</li></ul>\n<pre><code>code block</code></pre>",m:["Convert"]},
md2html:{n:"Markdown to HTML",i:'fa-right-left',c:'data',d:"Convert Markdown into HTML you can paste anywhere",p:"# Hello World\n\nThis is **bold** and *italic* with a [link](https://openvibe.network).\n\n- Item one\n- Item two\n\n> A quote\n\n`inline code`",m:["To HTML", "Preview"]},
md2html:{n:"Markdown to HTML",i:'fa-right-left',c:'data',d:"Convert Markdown into HTML you can paste anywhere",p:"# Hello World\n\nThis is **bold** and *italic* with a [link](https://openvibe.network).\n\n- Item one\n- Item two\n\n> A quote\n\n`inline code`",m:["To HTML", "Preview"]},
minify:{n:'Code Minifier',i:'fa-compress',c:'qual',d:'Minify JS, CSS, HTML & JSON',p:'{\n "name": "openvibe",\n "version": 1\n}',m:['JSON','HTML','CSS']},
minify:{n:'Code Minifier',i:'fa-compress',c:'qual',d:'Minify JS, CSS, HTML & JSON',p:'{\n "name": "openvibe",\n "version": 1\n}',m:['JSON','HTML','CSS']},
-codediff:{n:'Diff Checker',i:'fa-code-compare',c:'qual',d:'Side-by-side text diff checker',p:'Line one\nLine two\nLine three\nLine four',p2:'Line one\nLine 2 modified\nLine three\nLine five\nLine six',dual:1,m:['Compare']},
+codediff:{n:'Diff Checker',i:'fa-arrows-left-right',c:'qual',d:'Side-by-side text diff checker',p:'Line one\nLine two\nLine three\nLine four',p2:'Line one\nLine 2 modified\nLine three\nLine five\nLine six',dual:1,m:['Compare']},
regex:{n:'Regex Tester',i:'fa-magnifying-glass',c:'qual',d:'Regex tester with match highlighting',p:'\\b[A-Z][a-z]+\\b',p2:'Hello World from OpenVibe.Tools today',dual:1,m:['Test']},
regex:{n:'Regex Tester',i:'fa-magnifying-glass',c:'qual',d:'Regex tester with match highlighting',p:'\\b[A-Z][a-z]+\\b',p2:'Hello World from OpenVibe.Tools today',dual:1,m:['Test']},
slugify:{n:'Slug Generator',i:'fa-link',c:'qual',d:'URL slug generator',p:'Hello World! This is a Test -- 2024',m:['Generate']},
slugify:{n:'Slug Generator',i:'fa-link',c:'qual',d:'URL slug generator',p:'Hello World! This is a Test -- 2024',m:['Generate']},
-lorem:{n:'Lorem Ipsum Generator',i:'fa-paragraph',c:'qual',d:'Lorem ipsum generator',p:'',m:['1 Para','3 Para','5 Para','10 Sent','50 Words']},
+lorem:{n:'Lorem Ipsum Generator',i:'fa-align-left',c:'qual',d:'Lorem ipsum generator',p:'',m:['1 Para','3 Para','5 Para','10 Sent','50 Words']},
curlconvert:{n:'Online curl',i:'fa-terminal',c:'http',d:'Parse curl \u2192 fetch, Python, Node.js',p:"curl -X POST https://api.example.com/users -H 'Content-Type: application/json' -H 'Authorization: Bearer tok123' -d '{\"name\":\"openvibe\"}'",m:['To fetch','To Python','To Node','Parsed']},
curlconvert:{n:'Online curl',i:'fa-terminal',c:'http',d:'Parse curl \u2192 fetch, Python, Node.js',p:"curl -X POST https://api.example.com/users -H 'Content-Type: application/json' -H 'Authorization: Bearer tok123' -d '{\"name\":\"openvibe\"}'",m:['To fetch','To Python','To Node','Parsed']},
webhook:{n:'Webhook Tester',i:'fa-satellite-dish',c:'http',d:'Webhook request inspector bin',p:'',m:['Create Bin']},
webhook:{n:'Webhook Tester',i:'fa-satellite-dish',c:'http',d:'Webhook request inspector bin',p:'',m:['Create Bin']},
color:{n:'Color Converter',i:'fa-palette',c:'fe',d:'Color picker & HEX/RGB/HSL converter',p:'#a78bfa',m:['Convert']},
color:{n:'Color Converter',i:'fa-palette',c:'fe',d:'Color picker & HEX/RGB/HSL converter',p:'#a78bfa',m:['Convert']},
@@ -287,7 +286,7 @@ function showTool(){
@@ -287,7 +286,7 @@ function showTool(){
if(!framed){
if(!framed){
let ah='<button class="ab ab-go" onclick="run()"><i class="fa-solid fa-play"></i> Run</button>';
let ah='<button class="ab ab-go" onclick="run()"><i class="fa-solid fa-play"></i> Run</button>';
ah+='<button class="ab ab-s" onclick="copyOut()"><i class="fa-solid fa-copy"></i> Copy</button>';
ah+='<button class="ab ab-s" onclick="copyOut()"><i class="fa-solid fa-copy"></i> Copy</button>';
- ah+='<button class="ab ab-s" onclick="saveAsPaste(this)" title="Save the output as an unlisted paste on OpenVibe.Community"><i class="fa-solid fa-note-sticky"></i> Save as paste</button>';
+ ah+='<button class="ab ab-s" onclick="saveAsPaste(this)" title="Save the output as an unlisted paste on OpenVibe.Community"><i class="fa-solid fa-paste"></i> Save as paste</button>';
ah+='<button class="ab ab-s" onclick="clr()"><i class="fa-solid fa-trash"></i> Clear</button>';
ah+='<button class="ab ab-s" onclick="clr()"><i class="fa-solid fa-trash"></i> Clear</button>';
ah+='<button class="ab ab-s" onclick="sample()"><i class="fa-solid fa-flask"></i> Sample</button>';
ah+='<button class="ab ab-s" onclick="sample()"><i class="fa-solid fa-flask"></i> Sample</button>';
$('acts').innerHTML=ah;
$('acts').innerHTML=ah;
@@ -25,7 +25,6 @@
@@ -25,7 +25,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
:root {
:root {
@@ -142,7 +141,7 @@
@@ -142,7 +141,7 @@
'convert': { name: 'Converter', domain: 'convert.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },
'convert': { name: 'Converter', domain: 'convert.openvibe.tools', icon: 'fa-arrow-right-arrow-left', category: 'Convert' },
'compress': { name: 'Compress', domain: 'compress.openvibe.tools', icon: 'fa-compress', category: 'Optimize' },
'compress': { name: 'Compress', domain: 'compress.openvibe.tools', icon: 'fa-compress', category: 'Optimize' },
'resize': { name: 'Resize', domain: 'resize.openvibe.tools', icon: 'fa-expand', category: 'Resize' },
'resize': { name: 'Resize', domain: 'resize.openvibe.tools', icon: 'fa-expand', category: 'Resize' },
- 'crop': { name: 'Crop', domain: 'crop.openvibe.tools', icon: 'fa-crop', category: 'Edit' },
+ 'crop': { name: 'Crop', domain: 'crop.openvibe.tools', icon: 'fa-scissors', category: 'Edit' },
'png': { name: 'PNG', domain: 'png.openvibe.tools', icon: 'fa-image', category: 'Format' },
'png': { name: 'PNG', domain: 'png.openvibe.tools', icon: 'fa-image', category: 'Format' },
'jpg': { name: 'JPG', domain: 'jpg.openvibe.tools', icon: 'fa-image', category: 'Format' },
'jpg': { name: 'JPG', domain: 'jpg.openvibe.tools', icon: 'fa-image', category: 'Format' },
'webp': { name: 'WebP', domain: 'webp.openvibe.tools', icon: 'fa-image', category: 'Format' },
'webp': { name: 'WebP', domain: 'webp.openvibe.tools', icon: 'fa-image', category: 'Format' },
@@ -25,7 +25,6 @@
@@ -25,7 +25,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
<style>
:root {
:root {
--bg-primary: #0b0c12;
--bg-primary: #0b0c12;
@@ -671,21 +670,21 @@
@@ -671,21 +670,21 @@
net: { name: 'Network Tools', icon: 'fa-network-wired', cat: 'hub', endpoint: null, placeholder: 'Enter a domain, IP, or URL…' },
net: { name: 'Network Tools', icon: 'fa-network-wired', cat: 'hub', endpoint: null, placeholder: 'Enter a domain, IP, or URL…' },
lookup: { name: 'Domain & IP Lookup', icon: 'fa-magnifying-glass', cat: 'hub', endpoint: '/lookup', placeholder: 'Domain, IP, or URL — we\'ll look up everything', btnText: 'Lookup All' },
lookup: { name: 'Domain & IP Lookup', icon: 'fa-magnifying-glass', cat: 'hub', endpoint: '/lookup', placeholder: 'Domain, IP, or URL — we\'ll look up everything', btnText: 'Lookup All' },
// IP / Identity
// IP / Identity
- myip: { name: 'What Is My IP', icon: 'fa-location-crosshairs', cat: 'ip', endpoint: '/myip', placeholder: null, auto: true },
+ myip: { name: 'What Is My IP', icon: 'fa-location-dot', cat: 'ip', endpoint: '/myip', placeholder: null, auto: true },
ip: { name: 'IP Address Lookup', icon: 'fa-at', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
ip: { name: 'IP Address Lookup', icon: 'fa-at', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
ipv4: { name: 'IPv4 Lookup & CIDR Calculator', icon: 'fa-hashtag', cat: 'ip', endpoint: '/ipv4', placeholder: 'IPv4 address, CIDR block (e.g. 192.168.1.0/24) or domain' },
ipv4: { name: 'IPv4 Lookup & CIDR Calculator', icon: 'fa-hashtag', cat: 'ip', endpoint: '/ipv4', placeholder: 'IPv4 address, CIDR block (e.g. 192.168.1.0/24) or domain' },
ipv6: { name: 'IPv6 Lookup', icon: 'fa-code', cat: 'ip', endpoint: '/ipv6', placeholder: 'IPv6 address or domain' },
ipv6: { name: 'IPv6 Lookup', icon: 'fa-code', cat: 'ip', endpoint: '/ipv6', placeholder: 'IPv6 address or domain' },
geoip: { name: 'IP Geolocation', icon: 'fa-earth-americas', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
geoip: { name: 'IP Geolocation', icon: 'fa-earth-americas', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
hostname: { name: 'Hostname Lookup', icon: 'fa-server', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address to resolve hostname' },
hostname: { name: 'Hostname Lookup', icon: 'fa-server', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address to resolve hostname' },
- isp: { name: 'ISP Lookup', icon: 'fa-building', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
+ isp: { name: 'ISP Lookup', icon: 'fa-city', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
asn: { name: 'ASN Lookup', icon: 'fa-diagram-project', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
asn: { name: 'ASN Lookup', icon: 'fa-diagram-project', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
rdns: { name: 'Reverse DNS Lookup', icon: 'fa-rotate-left', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address for reverse DNS' },
rdns: { name: 'Reverse DNS Lookup', icon: 'fa-rotate-left', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address for reverse DNS' },
- whois: { name: 'Whois Lookup', icon: 'fa-address-book', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP' },
+ whois: { name: 'Whois Lookup', icon: 'fa-address-card', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP' },
rdap: { name: 'RDAP Lookup', icon: 'fa-id-card', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP for RDAP lookup' },
rdap: { name: 'RDAP Lookup', icon: 'fa-id-card', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP for RDAP lookup' },
// DNS
// DNS
dns: { name: 'DNS Lookup', icon: 'fa-sitemap', cat: 'dns', endpoint: '/dns', placeholder: 'Domain to query' },
dns: { name: 'DNS Lookup', icon: 'fa-sitemap', cat: 'dns', endpoint: '/dns', placeholder: 'Domain to query' },
dig: { name: 'Online dig', icon: 'fa-terminal', cat: 'dns', endpoint: '/dns', placeholder: 'Domain (dig-style query)', dnsTypes: true },
dig: { name: 'Online dig', icon: 'fa-terminal', cat: 'dns', endpoint: '/dns', placeholder: 'Domain (dig-style query)', dnsTypes: true },
- nslookup: { name: 'NSLookup', icon: 'fa-magnifying-glass-arrow-right', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for nslookup' },
+ nslookup: { name: 'NSLookup', icon: 'fa-magnifying-glass', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for nslookup' },
dnspropagation: { name: 'DNS Propagation Checker', icon: 'fa-globe', cat: 'dns', endpoint: '/dnsprop', placeholder: 'Domain to compare across public resolvers', propTypes: true },
dnspropagation: { name: 'DNS Propagation Checker', icon: 'fa-globe', cat: 'dns', endpoint: '/dnsprop', placeholder: 'Domain to compare across public resolvers', propTypes: true },
mx: { name: 'MX Record Lookup', icon: 'fa-envelope', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for MX records', defaultTypes: 'MX' },
mx: { name: 'MX Record Lookup', icon: 'fa-envelope', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for MX records', defaultTypes: 'MX' },
txt: { name: 'TXT Record Lookup', icon: 'fa-file-lines', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for TXT records', defaultTypes: 'TXT' },
txt: { name: 'TXT Record Lookup', icon: 'fa-file-lines', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for TXT records', defaultTypes: 'TXT' },
@@ -699,7 +698,7 @@
@@ -699,7 +698,7 @@
mtr: { name: 'MTR Report', icon: 'fa-chart-line', cat: 'diag', endpoint: '/mtr', placeholder: 'Host for an MTR report (10 probes a hop, from our server)', wait: 'about 15 seconds' },
mtr: { name: 'MTR Report', icon: 'fa-chart-line', cat: 'diag', endpoint: '/mtr', placeholder: 'Host for an MTR report (10 probes a hop, from our server)', wait: 'about 15 seconds' },
port: { name: 'Port Checker', icon: 'fa-door-open', cat: 'diag', endpoint: '/port', placeholder: 'Host to scan ports' },
port: { name: 'Port Checker', icon: 'fa-door-open', cat: 'diag', endpoint: '/port', placeholder: 'Host to scan ports' },
headers: { name: 'HTTP Header Checker', icon: 'fa-list', cat: 'diag', endpoint: '/headers', placeholder: 'URL or domain' },
headers: { name: 'HTTP Header Checker', icon: 'fa-list', cat: 'diag', endpoint: '/headers', placeholder: 'URL or domain' },
- redirects: { name: 'Redirect Checker', icon: 'fa-share', cat: 'diag', endpoint: '/redirects', placeholder: 'URL to follow redirects' },
+ redirects: { name: 'Redirect Checker', icon: 'fa-arrow-up-from-bracket', cat: 'diag', endpoint: '/redirects', placeholder: 'URL to follow redirects' },
ssl: { name: 'SSL Certificate Checker', icon: 'fa-lock', cat: 'diag', endpoint: '/ssl', placeholder: 'Domain for SSL check' },
ssl: { name: 'SSL Certificate Checker', icon: 'fa-lock', cat: 'diag', endpoint: '/ssl', placeholder: 'Domain for SSL check' },
curl: { name: 'Online curl', icon: 'fa-download', cat: 'diag', endpoint: '/headers', placeholder: 'URL' },
curl: { name: 'Online curl', icon: 'fa-download', cat: 'diag', endpoint: '/headers', placeholder: 'URL' },
httpstatus: { name: 'HTTP Status Codes', icon: 'fa-circle-check', cat: 'diag', endpoint: '/headers', placeholder: 'URL to check' },
httpstatus: { name: 'HTTP Status Codes', icon: 'fa-circle-check', cat: 'diag', endpoint: '/headers', placeholder: 'URL to check' },
@@ -715,7 +714,7 @@
@@ -715,7 +714,7 @@
};
};
const CATS = {
const CATS = {
- ip: { label: 'IP / Identity / Ownership', icon: 'fa-fingerprint' },
+ ip: { label: 'IP / Identity / Ownership', icon: 'fa-id-card' },
dns: { label: 'DNS Tools', icon: 'fa-sitemap' },
dns: { label: 'DNS Tools', icon: 'fa-sitemap' },
diag: { label: 'Active Diagnostics', icon: 'fa-satellite-dish' },
diag: { label: 'Active Diagnostics', icon: 'fa-satellite-dish' },
mail: { label: 'Email & Reputation', icon: 'fa-envelope' },
mail: { label: 'Email & Reputation', icon: 'fa-envelope' },
@@ -31,22 +31,22 @@ const DEV_TOOLS = [
@@ -31,22 +31,22 @@ const DEV_TOOLS = [
{ id: 'base64', subdomain: 'base64', name: 'OpenVibeBase64', icon: 'fa-lock', desc: 'Base64 encode & decode text and files', category: 'encoding' },
{ id: 'base64', subdomain: 'base64', name: 'OpenVibeBase64', icon: 'fa-lock', desc: 'Base64 encode & decode text and files', category: 'encoding' },
{ id: 'url', subdomain: 'url', name: 'OpenVibeURL', icon: 'fa-link', desc: 'URL encode, decode & component parser', category: 'encoding' },
{ id: 'url', subdomain: 'url', name: 'OpenVibeURL', icon: 'fa-link', desc: 'URL encode, decode & component parser', category: 'encoding' },
{ id: 'jwt', subdomain: 'jwt', name: 'OpenVibeJWT', icon: 'fa-key', desc: 'JWT decoder — inspect header, payload & expiry', category: 'encoding' },
{ id: 'jwt', subdomain: 'jwt', name: 'OpenVibeJWT', icon: 'fa-key', desc: 'JWT decoder — inspect header, payload & expiry', category: 'encoding' },
- { id: 'uuid', subdomain: 'uuid', name: 'OpenVibeUUID', icon: 'fa-fingerprint', desc: 'UUID v4 generator, bulk generate & validator', category: 'encoding' },
+ { id: 'uuid', subdomain: 'uuid', name: 'OpenVibeUUID', icon: 'fa-id-card', desc: 'UUID v4 generator, bulk generate & validator', category: 'encoding' },
{ id: 'hash', subdomain: 'hash', name: 'OpenVibeHash', icon: 'fa-hashtag', desc: 'SHA-1, SHA-256 & SHA-512 hash generator', category: 'encoding' },
{ id: 'hash', subdomain: 'hash', name: 'OpenVibeHash', icon: 'fa-hashtag', desc: 'SHA-1, SHA-256 & SHA-512 hash generator', category: 'encoding' },
- { id: 'hex', subdomain: 'hex', name: 'OpenVibeHex', icon: 'fa-barcode', desc: 'Hex encoder/decoder & binary converter', category: 'encoding' },
+ { id: 'hex', subdomain: 'hex', name: 'OpenVibeHex', icon: 'fa-code', desc: 'Hex encoder/decoder & binary converter', category: 'encoding' },
{ id: 'escape', subdomain: 'entities', name: 'OpenVibeEscape', icon: 'fa-shield-halved', desc: 'HTML, JavaScript & URL escape/unescape', category: 'encoding' },
{ id: 'escape', subdomain: 'entities', name: 'OpenVibeEscape', icon: 'fa-shield-halved', desc: 'HTML, JavaScript & URL escape/unescape', category: 'encoding' },
// Time & Scheduling
// Time & Scheduling
{ id: 'timestamp', subdomain: 'timestamp', name: 'OpenVibeTimestamp', icon: 'fa-clock', desc: 'Unix timestamp ↔ date converter with timezones', category: 'time' },
{ id: 'timestamp', subdomain: 'timestamp', name: 'OpenVibeTimestamp', icon: 'fa-clock', desc: 'Unix timestamp ↔ date converter with timezones', category: 'time' },
- { id: 'cron', subdomain: 'cron', name: 'OpenVibeCron', icon: 'fa-calendar-check', desc: 'Cron expression parser with next run calculator', category: 'time' },
+ { id: 'cron', subdomain: 'cron', name: 'OpenVibeCron', icon: 'fa-calendar-days', desc: 'Cron expression parser with next run calculator', category: 'time' },
// Code Quality
// Code Quality
{ id: 'beautify', subdomain: 'beautify', name: 'OpenVibeBeautify', icon: 'fa-wand-magic-sparkles', desc: 'Beautify & format JS, HTML, CSS, JSON & SQL', category: 'quality' },
{ id: 'beautify', subdomain: 'beautify', name: 'OpenVibeBeautify', icon: 'fa-wand-magic-sparkles', desc: 'Beautify & format JS, HTML, CSS, JSON & SQL', category: 'quality' },
{ id: 'minify', subdomain: 'minify', name: 'OpenVibeMinify', icon: 'fa-compress', desc: 'Minify JS, HTML, CSS & JSON', category: 'quality' },
{ id: 'minify', subdomain: 'minify', name: 'OpenVibeMinify', icon: 'fa-compress', desc: 'Minify JS, HTML, CSS & JSON', category: 'quality' },
- { id: 'diff', subdomain: 'codediff', name: 'OpenVibeDiff', icon: 'fa-code-compare', desc: 'Side-by-side text & code diff checker', category: 'quality' },
+ { id: 'diff', subdomain: 'codediff', name: 'OpenVibeDiff', icon: 'fa-arrows-left-right', desc: 'Side-by-side text & code diff checker', category: 'quality' },
{ id: 'regex', subdomain: 'regex', name: 'OpenVibeRegex', icon: 'fa-magnifying-glass', desc: 'Regex tester with match highlighting & capture groups', category: 'quality' },
{ id: 'regex', subdomain: 'regex', name: 'OpenVibeRegex', icon: 'fa-magnifying-glass', desc: 'Regex tester with match highlighting & capture groups', category: 'quality' },
{ id: 'slug', subdomain: 'slugify', name: 'OpenVibeSlug', icon: 'fa-link', desc: 'URL slug generator from any text', category: 'quality' },
{ id: 'slug', subdomain: 'slugify', name: 'OpenVibeSlug', icon: 'fa-link', desc: 'URL slug generator from any text', category: 'quality' },
- { id: 'lorem', subdomain: 'lorem', name: 'OpenVibeLorem', icon: 'fa-paragraph', desc: 'Lorem ipsum & placeholder text generator', category: 'quality' },
+ { id: 'lorem', subdomain: 'lorem', name: 'OpenVibeLorem', icon: 'fa-align-left', desc: 'Lorem ipsum & placeholder text generator', category: 'quality' },
// HTTP & API
// HTTP & API
{ id: 'curl', subdomain: 'curlconvert', name: 'OpenVibeCurl', icon: 'fa-terminal', desc: 'Parse curl commands & convert to fetch, Python, Node.js', category: 'http' },
{ id: 'curl', subdomain: 'curlconvert', name: 'OpenVibeCurl', icon: 'fa-terminal', desc: 'Parse curl commands & convert to fetch, Python, Node.js', category: 'http' },
@@ -130,9 +130,9 @@ app.use(helmet({
@@ -130,9 +130,9 @@ app.use(helmet({
directives: {
directives: {
defaultSrc: ["'self'"],
defaultSrc: ["'self'"],
// 'wasm-unsafe-eval': the barcode scanner's ZXing reader is WebAssembly (codes.html), served from /vendor here.
// 'wasm-unsafe-eval': the barcode scanner's ZXing reader is WebAssembly (codes.html), served from /vendor here.
- scriptSrc: ["'self'", "'unsafe-inline'", "'wasm-unsafe-eval'", "https://openvibe.network", "https://openvibe.live", "cdnjs.cloudflare.com", "cdn.jsdelivr.net", "fonts.googleapis.com", "https://static.cloudflareinsights.com"],
- styleSrc: ["'self'", "'unsafe-inline'", "cdnjs.cloudflare.com", "fonts.googleapis.com", "fonts.gstatic.com"],
- fontSrc: ["'self'", "fonts.gstatic.com", "cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "'wasm-unsafe-eval'", "https://openvibe.network", "https://openvibe.live", "cdn.jsdelivr.net", "fonts.googleapis.com", "https://static.cloudflareinsights.com"],
+ styleSrc: ["'self'", "'unsafe-inline'", "fonts.googleapis.com", "fonts.gstatic.com"],
+ fontSrc: ["'self'", "fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "blob:", "https://openvibe.network", "https://openvibe.media"],
imgSrc: ["'self'", "data:", "blob:", "https://openvibe.network", "https://openvibe.media"],
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
connectSrc: ["'self'", "https://cloudflareinsights.com", "https://openvibe.network", "https://openvibe.tools", "https://*.openvibe.tools", "https://openvibe.media", "https://openvibe.live", "https://openvibe.games", "https://openvibe.events"],
connectSrc: ["'self'", "https://cloudflareinsights.com", "https://openvibe.network", "https://openvibe.tools", "https://*.openvibe.tools", "https://openvibe.media", "https://openvibe.live", "https://openvibe.games", "https://openvibe.events"],
@@ -94,21 +94,21 @@ const NET_TOOLS = [
@@ -94,21 +94,21 @@ const NET_TOOLS = [
{ id: 'net', subdomain: 'net', name: 'Net.OpenVibe', icon: 'fa-network-wired', desc: 'Network & Internet Diagnostics Hub', hub: true },
{ id: 'net', subdomain: 'net', name: 'Net.OpenVibe', icon: 'fa-network-wired', desc: 'Network & Internet Diagnostics Hub', hub: true },
{ id: 'lookup', subdomain: 'lookup', name: 'OpenVibeLookup', icon: 'fa-magnifying-glass', desc: 'All-in-one domain/IP lookup supertool', endpoint: '/lookup' },
{ id: 'lookup', subdomain: 'lookup', name: 'OpenVibeLookup', icon: 'fa-magnifying-glass', desc: 'All-in-one domain/IP lookup supertool', endpoint: '/lookup' },
// IP / Identity
// IP / Identity
- { id: 'myip', subdomain: 'myip', name: 'OpenVibeMyIP', icon: 'fa-location-crosshairs', desc: 'Your public IP, ISP, location & reverse DNS', endpoint: '/myip' },
+ { id: 'myip', subdomain: 'myip', name: 'OpenVibeMyIP', icon: 'fa-location-dot', desc: 'Your public IP, ISP, location & reverse DNS', endpoint: '/myip' },
{ id: 'ip', subdomain: 'ip', name: 'OpenVibeIP', icon: 'fa-at', desc: 'IP address lookup & details (IPv4 & IPv6)', endpoint: '/ip' },
{ id: 'ip', subdomain: 'ip', name: 'OpenVibeIP', icon: 'fa-at', desc: 'IP address lookup & details (IPv4 & IPv6)', endpoint: '/ip' },
{ id: 'ipv4', subdomain: 'ipv4', name: 'OpenVibeIPv4', icon: 'fa-hashtag', desc: 'IPv4 address lookup, class, CIDR calculator', endpoint: '/ipv4' },
{ id: 'ipv4', subdomain: 'ipv4', name: 'OpenVibeIPv4', icon: 'fa-hashtag', desc: 'IPv4 address lookup, class, CIDR calculator', endpoint: '/ipv4' },
{ id: 'ipv6', subdomain: 'ipv6', name: 'OpenVibeIPv6', icon: 'fa-code', desc: 'IPv6 address lookup, expansion, type detection', endpoint: '/ipv6' },
{ id: 'ipv6', subdomain: 'ipv6', name: 'OpenVibeIPv6', icon: 'fa-code', desc: 'IPv6 address lookup, expansion, type detection', endpoint: '/ipv6' },
{ id: 'geoip', subdomain: 'geoip', name: 'OpenVibeGeoIP', icon: 'fa-earth-americas', desc: 'IP geolocation — country, city, timezone, map', endpoint: '/ip' },
{ id: 'geoip', subdomain: 'geoip', name: 'OpenVibeGeoIP', icon: 'fa-earth-americas', desc: 'IP geolocation — country, city, timezone, map', endpoint: '/ip' },
{ id: 'hostname', subdomain: 'hostname', name: 'OpenVibeHostname', icon: 'fa-server', desc: 'Hostname & reverse DNS lookup', endpoint: '/rdns' },
{ id: 'hostname', subdomain: 'hostname', name: 'OpenVibeHostname', icon: 'fa-server', desc: 'Hostname & reverse DNS lookup', endpoint: '/rdns' },
- { id: 'isp', subdomain: 'isp', name: 'OpenVibeISP', icon: 'fa-building', desc: 'ISP & organization info from IP', endpoint: '/ip' },
+ { id: 'isp', subdomain: 'isp', name: 'OpenVibeISP', icon: 'fa-city', desc: 'ISP & organization info from IP', endpoint: '/ip' },
{ id: 'asn', subdomain: 'asn', name: 'OpenVibeASN', icon: 'fa-diagram-project', desc: 'Autonomous System Number lookup', endpoint: '/ip' },
{ id: 'asn', subdomain: 'asn', name: 'OpenVibeASN', icon: 'fa-diagram-project', desc: 'Autonomous System Number lookup', endpoint: '/ip' },
{ id: 'rdns', subdomain: 'rdns', name: 'OpenVibeReverseDNS', icon: 'fa-rotate-left', desc: 'Reverse DNS (PTR) lookup for IPv4 & IPv6', endpoint: '/rdns' },
{ id: 'rdns', subdomain: 'rdns', name: 'OpenVibeReverseDNS', icon: 'fa-rotate-left', desc: 'Reverse DNS (PTR) lookup for IPv4 & IPv6', endpoint: '/rdns' },
- { id: 'whois', subdomain: 'whois', name: 'OpenVibeWhois', icon: 'fa-address-book', desc: 'Whois domain registration lookup', endpoint: '/rdap' },
+ { id: 'whois', subdomain: 'whois', name: 'OpenVibeWhois', icon: 'fa-address-card', desc: 'Whois domain registration lookup', endpoint: '/rdap' },
{ id: 'rdap', subdomain: 'rdap', name: 'OpenVibeRDAP', icon: 'fa-id-card', desc: 'RDAP — modern registration data lookup', endpoint: '/rdap' },
{ id: 'rdap', subdomain: 'rdap', name: 'OpenVibeRDAP', icon: 'fa-id-card', desc: 'RDAP — modern registration data lookup', endpoint: '/rdap' },
// DNS
// DNS
{ id: 'dns', subdomain: 'dns', name: 'OpenVibeDNS', icon: 'fa-sitemap', desc: 'DNS record lookup — A, AAAA, MX, TXT, and more', endpoint: '/dns' },
{ id: 'dns', subdomain: 'dns', name: 'OpenVibeDNS', icon: 'fa-sitemap', desc: 'DNS record lookup — A, AAAA, MX, TXT, and more', endpoint: '/dns' },
{ id: 'dig', subdomain: 'dig', name: 'OpenVibeDig', icon: 'fa-terminal', desc: 'Online dig command — query any DNS record', endpoint: '/dns' },
{ id: 'dig', subdomain: 'dig', name: 'OpenVibeDig', icon: 'fa-terminal', desc: 'Online dig command — query any DNS record', endpoint: '/dns' },
- { id: 'nslookup', subdomain: 'nslookup', name: 'OpenVibeNSLookup', icon: 'fa-magnifying-glass-arrow-right', desc: 'NSLookup — simple DNS query tool', endpoint: '/dns' },
+ { id: 'nslookup', subdomain: 'nslookup', name: 'OpenVibeNSLookup', icon: 'fa-magnifying-glass', desc: 'NSLookup — simple DNS query tool', endpoint: '/dns' },
{ id: 'dnspropagation', subdomain: 'dnspropagation', name: 'OpenVibeDNSPropagation', icon: 'fa-globe', desc: 'Check DNS propagation across world regions', endpoint: '/dnsprop' },
{ id: 'dnspropagation', subdomain: 'dnspropagation', name: 'OpenVibeDNSPropagation', icon: 'fa-globe', desc: 'Check DNS propagation across world regions', endpoint: '/dnsprop' },
{ id: 'mx', subdomain: 'mx', name: 'OpenVibeMX', icon: 'fa-envelope', desc: 'MX record lookup — mail server discovery', endpoint: '/dns' },
{ id: 'mx', subdomain: 'mx', name: 'OpenVibeMX', icon: 'fa-envelope', desc: 'MX record lookup — mail server discovery', endpoint: '/dns' },
{ id: 'txt', subdomain: 'txt', name: 'OpenVibeTXT', icon: 'fa-file-lines', desc: 'TXT record lookup — SPF, DKIM, verification', endpoint: '/dns' },
{ id: 'txt', subdomain: 'txt', name: 'OpenVibeTXT', icon: 'fa-file-lines', desc: 'TXT record lookup — SPF, DKIM, verification', endpoint: '/dns' },
@@ -122,7 +122,7 @@ const NET_TOOLS = [
@@ -122,7 +122,7 @@ const NET_TOOLS = [
{ id: 'mtr', subdomain: 'mtr', name: 'OpenVibeMTR', icon: 'fa-chart-line', desc: 'MTR — combined ping + traceroute analysis', endpoint: '/mtr' },
{ id: 'mtr', subdomain: 'mtr', name: 'OpenVibeMTR', icon: 'fa-chart-line', desc: 'MTR — combined ping + traceroute analysis', endpoint: '/mtr' },
{ id: 'port', subdomain: 'port', name: 'OpenVibePortCheck', icon: 'fa-door-open', desc: 'Port scanner — check if ports are open', endpoint: '/port' },
{ id: 'port', subdomain: 'port', name: 'OpenVibePortCheck', icon: 'fa-door-open', desc: 'Port scanner — check if ports are open', endpoint: '/port' },
{ id: 'headers', subdomain: 'headers', name: 'OpenVibeHeaders', icon: 'fa-list', desc: 'HTTP headers checker — security & cache analysis', endpoint: '/headers' },
{ id: 'headers', subdomain: 'headers', name: 'OpenVibeHeaders', icon: 'fa-list', desc: 'HTTP headers checker — security & cache analysis', endpoint: '/headers' },
- { id: 'redirects', subdomain: 'redirects', name: 'OpenVibeRedirects', icon: 'fa-share', desc: 'Redirect chain tracer — follow all hops', endpoint: '/redirects' },
+ { id: 'redirects', subdomain: 'redirects', name: 'OpenVibeRedirects', icon: 'fa-arrow-up-from-bracket', desc: 'Redirect chain tracer — follow all hops', endpoint: '/redirects' },
{ id: 'ssl', subdomain: 'ssl', name: 'OpenVibeSSL', icon: 'fa-lock', desc: 'SSL/TLS certificate checker & chain inspector', endpoint: '/ssl' },
{ id: 'ssl', subdomain: 'ssl', name: 'OpenVibeSSL', icon: 'fa-lock', desc: 'SSL/TLS certificate checker & chain inspector', endpoint: '/ssl' },
{ id: 'curl', subdomain: 'curl', name: 'OpenVibeCurl', icon: 'fa-download', desc: 'Online curl — HTTP request tester', endpoint: '/headers' },
{ id: 'curl', subdomain: 'curl', name: 'OpenVibeCurl', icon: 'fa-download', desc: 'Online curl — HTTP request tester', endpoint: '/headers' },
{ id: 'httpstatus', subdomain: 'httpstatus', name: 'OpenVibeHTTPStatus', icon: 'fa-circle-check', desc: 'HTTP status code reference & checker', endpoint: '/headers' },
{ id: 'httpstatus', subdomain: 'httpstatus', name: 'OpenVibeHTTPStatus', icon: 'fa-circle-check', desc: 'HTTP status code reference & checker', endpoint: '/headers' },
@@ -21,7 +21,7 @@
@@ -21,7 +21,7 @@
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"sharp": "^0.33.5",
"sharp": "^0.33.5",
"to-ico": "^1.1.5",
"to-ico": "^1.1.5",
@@ -2264,9 +2264,9 @@
@@ -2264,9 +2264,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.56.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
+ "version": "4.73.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
+ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -20,7 +20,6 @@
@@ -20,7 +20,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/style.css">
</head>
</head>
<body>
<body>
@@ -39,7 +38,7 @@
@@ -39,7 +38,7 @@
<button class="tool-tab active" data-tool="convert"><i class="fa-solid fa-arrows-rotate"></i> Convert</button>
<button class="tool-tab active" data-tool="convert"><i class="fa-solid fa-arrows-rotate"></i> Convert</button>
<button class="tool-tab" data-tool="compress"><i class="fa-solid fa-compress"></i> Compress</button>
<button class="tool-tab" data-tool="compress"><i class="fa-solid fa-compress"></i> Compress</button>
<button class="tool-tab" data-tool="resize"><i class="fa-solid fa-up-right-and-down-left-from-center"></i> Resize</button>
<button class="tool-tab" data-tool="resize"><i class="fa-solid fa-up-right-and-down-left-from-center"></i> Resize</button>
- <button class="tool-tab" data-tool="crop"><i class="fa-solid fa-crop-simple"></i> Crop</button>
+ <button class="tool-tab" data-tool="crop"><i class="fa-solid fa-scissors"></i> Crop</button>
</nav>
</nav>
<!-- Upload Zone -->
<!-- Upload Zone -->
@@ -152,7 +151,7 @@
@@ -152,7 +151,7 @@
<i class="fa-solid fa-download"></i> Download
<i class="fa-solid fa-download"></i> Download
</a>
</a>
<button class="btn btn-secondary" id="another-btn">
<button class="btn btn-secondary" id="another-btn">
- <i class="fa-solid fa-arrow-rotate-left"></i> Process Another
+ <i class="fa-solid fa-rotate-left"></i> Process Another
</button>
</button>
</div>
</div>
</section>
</section>
@@ -519,14 +519,14 @@
@@ -519,14 +519,14 @@
{ host: 'webp.openvibe.tools', name: 'OpenVibeWebP', icon: 'fa-file-image', desc: 'Convert to WebP' },
{ host: 'webp.openvibe.tools', name: 'OpenVibeWebP', icon: 'fa-file-image', desc: 'Convert to WebP' },
{ host: 'avif.openvibe.tools', name: 'OpenVibeAVIF', icon: 'fa-file-image', desc: 'Convert to AVIF' },
{ host: 'avif.openvibe.tools', name: 'OpenVibeAVIF', icon: 'fa-file-image', desc: 'Convert to AVIF' },
{ host: 'heic.openvibe.tools', name: 'OpenVibeHEIC', icon: 'fa-file-image', desc: 'HEIC Converter' },
{ host: 'heic.openvibe.tools', name: 'OpenVibeHEIC', icon: 'fa-file-image', desc: 'HEIC Converter' },
- { host: 'svg.openvibe.tools', name: 'OpenVibeSVG', icon: 'fa-bezier-curve', desc: 'SVG Converter' },
+ { host: 'svg.openvibe.tools', name: 'OpenVibeSVG', icon: 'fa-shapes', desc: 'SVG Converter' },
{ host: 'gif.openvibe.tools', name: 'OpenVibeGIF', icon: 'fa-film', desc: 'GIF Converter' },
{ host: 'gif.openvibe.tools', name: 'OpenVibeGIF', icon: 'fa-film', desc: 'GIF Converter' },
{ host: 'ico.openvibe.tools', name: 'OpenVibeICO', icon: 'fa-icons', desc: 'ICO Favicon Maker' },
{ host: 'ico.openvibe.tools', name: 'OpenVibeICO', icon: 'fa-icons', desc: 'ICO Favicon Maker' },
{ host: 'tiff.openvibe.tools', name: 'OpenVibeTIFF', icon: 'fa-file-image', desc: 'Convert to TIFF' },
{ host: 'tiff.openvibe.tools', name: 'OpenVibeTIFF', icon: 'fa-file-image', desc: 'Convert to TIFF' },
{ host: 'bmp.openvibe.tools', name: 'OpenVibeBMP', icon: 'fa-file-image', desc: 'Convert to BMP' },
{ host: 'bmp.openvibe.tools', name: 'OpenVibeBMP', icon: 'fa-file-image', desc: 'Convert to BMP' },
{ host: 'compress.openvibe.tools', name: 'OpenVibeCompress', icon: 'fa-compress', desc: 'Compress Images' },
{ host: 'compress.openvibe.tools', name: 'OpenVibeCompress', icon: 'fa-compress', desc: 'Compress Images' },
{ host: 'resize.openvibe.tools', name: 'OpenVibeResize', icon: 'fa-up-right-and-down-left-from-center', desc: 'Resize Images' },
{ host: 'resize.openvibe.tools', name: 'OpenVibeResize', icon: 'fa-up-right-and-down-left-from-center', desc: 'Resize Images' },
- { host: 'crop.openvibe.tools', name: 'OpenVibeCrop', icon: 'fa-crop-simple', desc: 'Crop Images' },
+ { host: 'crop.openvibe.tools', name: 'OpenVibeCrop', icon: 'fa-scissors', desc: 'Crop Images' },
{ host: 'convert.openvibe.tools', name: 'OpenVibeConvert', icon: 'fa-arrows-rotate', desc: 'Format Converter' },
{ host: 'convert.openvibe.tools', name: 'OpenVibeConvert', icon: 'fa-arrows-rotate', desc: 'Format Converter' },
{ host: 'favicon.openvibe.tools', name: 'OpenVibeFavicon', icon: 'fa-icons', desc: 'Favicon Generator' },
{ host: 'favicon.openvibe.tools', name: 'OpenVibeFavicon', icon: 'fa-icons', desc: 'Favicon Generator' },
];
];
@@ -60,7 +60,7 @@ const DOMAIN_MAP = {
@@ -60,7 +60,7 @@ const DOMAIN_MAP = {
},
},
'svg.openvibe.tools': {
'svg.openvibe.tools': {
toolId: 'svg', brandName: 'OpenVibeSVG', defaultOp: 'convert', defaultFormat: 'png',
toolId: 'svg', brandName: 'OpenVibeSVG', defaultOp: 'convert', defaultFormat: 'png',
- faIcon: 'fa-bezier-curve',
+ faIcon: 'fa-shapes',
seoTitle: 'OpenVibeSVG — Convert SVG Images Online',
seoTitle: 'OpenVibeSVG — Convert SVG Images Online',
seoDescription: 'Convert SVG to PNG, JPG, WebP. Convert bitmap images to SVG vector traces. Online SVG converter.',
seoDescription: 'Convert SVG to PNG, JPG, WebP. Convert bitmap images to SVG vector traces. Online SVG converter.',
},
},
@@ -104,7 +104,7 @@ const DOMAIN_MAP = {
@@ -104,7 +104,7 @@ const DOMAIN_MAP = {
},
},
'crop.openvibe.tools': {
'crop.openvibe.tools': {
toolId: 'crop', brandName: 'OpenVibeCrop', defaultOp: 'crop',
toolId: 'crop', brandName: 'OpenVibeCrop', defaultOp: 'crop',
- faIcon: 'fa-crop-simple',
+ faIcon: 'fa-scissors',
seoTitle: 'OpenVibeCrop — Crop Images Online',
seoTitle: 'OpenVibeCrop — Crop Images Online',
seoDescription: 'Crop images by custom dimensions or preset aspect ratios (16:9, 4:3, 1:1). Online image cropper.',
seoDescription: 'Crop images by custom dimensions or preset aspect ratios (16:9, 4:3, 1:1). Online image cropper.',
},
},
@@ -101,9 +101,9 @@ app.use(helmet({
@@ -101,9 +101,9 @@ app.use(helmet({
contentSecurityPolicy: {
contentSecurityPolicy: {
directives: {
directives: {
defaultSrc: ["'self'"],
defaultSrc: ["'self'"],
- scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://openvibe.network"],
- styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
- fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://openvibe.network"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "blob:", jobsRuntime.mediaOrigin()], // job result previews are 302s to Media
imgSrc: ["'self'", "data:", "blob:", jobsRuntime.mediaOrigin()], // job result previews are 302s to Media
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
connectSrc: ["'self'", "https://openvibe.network", "https://*.openvibe.tools", "https://openvibe.events"],
connectSrc: ["'self'", "https://openvibe.network", "https://*.openvibe.tools", "https://openvibe.events"],
@@ -43,7 +43,7 @@ const TOOLS = {
@@ -43,7 +43,7 @@ const TOOLS = {
id: 'crop',
id: 'crop',
label: 'Crop',
label: 'Crop',
description: 'Crop images to specific areas or aspect ratios',
description: 'Crop images to specific areas or aspect ratios',
- faIcon: 'fa-crop-simple',
+ faIcon: 'fa-scissors',
accepts: ['png', 'jpg', 'jpeg', 'webp', 'avif', 'tiff', 'bmp', 'gif', 'heic', 'heif', 'svg'],
accepts: ['png', 'jpg', 'jpeg', 'webp', 'avif', 'tiff', 'bmp', 'gif', 'heic', 'heif', 'svg'],
outputs: null, // same as input
outputs: null, // same as input
handler: crop,
handler: crop,
@@ -21,7 +21,7 @@
@@ -21,7 +21,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"node-cache": "^5.1.2",
"node-cache": "^5.1.2",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
}
}
},
},
@@ -1446,9 +1446,9 @@
@@ -1446,9 +1446,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.56.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
+ "version": "4.73.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
+ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -35,8 +35,6 @@
@@ -35,8 +35,6 @@
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
- <!-- Font Awesome 6 -->
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Leaflet -->
<!-- Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css">
@@ -73,7 +71,7 @@
@@ -73,7 +71,7 @@
</div>
</div>
<div class="splash-sources">
<div class="splash-sources">
<span><i class="fa-solid fa-database"></i> RIDB</span>
<span><i class="fa-solid fa-database"></i> RIDB</span>
- <span><i class="fa-brands fa-openstreetmap"></i> OSM</span>
+ <span><i class="fa-solid fa-map"></i> OSM</span>
<span><i class="fa-solid fa-campground"></i> FreeCampsites</span>
<span><i class="fa-solid fa-campground"></i> FreeCampsites</span>
<span><i class="fa-solid fa-globe"></i> iOverlander</span>
<span><i class="fa-solid fa-globe"></i> iOverlander</span>
<span><i class="fa-solid fa-bridge"></i> Bridges</span>
<span><i class="fa-solid fa-bridge"></i> Bridges</span>
@@ -135,7 +133,7 @@
@@ -135,7 +133,7 @@
<div class="search-box">
<div class="search-box">
<i class="fa-solid fa-magnifying-glass search-icon"></i>
<i class="fa-solid fa-magnifying-glass search-icon"></i>
<input type="text" id="search-input" placeholder="Address, city, or coordinates..." autocomplete="off">
<input type="text" id="search-input" placeholder="Address, city, or coordinates..." autocomplete="off">
- <button id="btn-my-location" title="Use GPS"><i class="fa-solid fa-crosshairs"></i></button>
+ <button id="btn-my-location" title="Use GPS"><i class="fa-solid fa-location-dot"></i></button>
<select id="search-radius" title="Search radius">
<select id="search-radius" title="Search radius">
<option value="5">5 mi</option>
<option value="5">5 mi</option>
<option value="10">10 mi</option>
<option value="10">10 mi</option>
@@ -152,8 +150,8 @@
@@ -152,8 +150,8 @@
<button id="btn-food" class="header-btn" title="Food Banks & Grocery"><i class="fa-solid fa-utensils"></i></button>
<button id="btn-food" class="header-btn" title="Food Banks & Grocery"><i class="fa-solid fa-utensils"></i></button>
<button id="btn-favorites" class="header-btn" title="Favorites"><i class="fa-solid fa-heart"></i></button>
<button id="btn-favorites" class="header-btn" title="Favorites"><i class="fa-solid fa-heart"></i></button>
<button id="btn-add-spot" class="header-btn" title="Add Custom Spot"><i class="fa-solid fa-plus"></i></button>
<button id="btn-add-spot" class="header-btn" title="Add Custom Spot"><i class="fa-solid fa-plus"></i></button>
- <button id="btn-guide" class="header-btn" title="Survival Guide"><i class="fa-solid fa-book-skull"></i></button>
- <button id="btn-stats" class="header-btn" title="Statistics"><i class="fa-solid fa-chart-pie"></i></button>
+ <button id="btn-guide" class="header-btn" title="Survival Guide"><i class="fa-solid fa-book"></i></button>
+ <button id="btn-stats" class="header-btn" title="Statistics"><i class="fa-solid fa-chart-column"></i></button>
<button id="btn-theme" class="header-btn" title="Toggle theme"><i class="fa-solid fa-moon"></i></button>
<button id="btn-theme" class="header-btn" title="Toggle theme"><i class="fa-solid fa-moon"></i></button>
</div>
</div>
</header>
</header>
@@ -187,7 +185,7 @@
@@ -187,7 +185,7 @@
</select>
</select>
<button id="btn-heatmap" class="filter-tool-btn" title="Stealth heatmap"><i class="fa-solid fa-fire"></i></button>
<button id="btn-heatmap" class="filter-tool-btn" title="Stealth heatmap"><i class="fa-solid fa-fire"></i></button>
<button id="btn-crime-heatmap" class="filter-tool-btn" title="Crime/sketch heatmap"><i class="fa-solid fa-skull-crossbones"></i></button>
<button id="btn-crime-heatmap" class="filter-tool-btn" title="Crime/sketch heatmap"><i class="fa-solid fa-skull-crossbones"></i></button>
- <button id="btn-satellite" class="filter-tool-btn" title="Satellite view"><i class="fa-solid fa-satellite"></i></button>
+ <button id="btn-satellite" class="filter-tool-btn" title="Satellite view"><i class="fa-solid fa-satellite-dish"></i></button>
</div>
</div>
</div>
</div>
@@ -214,7 +212,7 @@
@@ -214,7 +212,7 @@
<div class="mini-stat"><i class="fa-solid fa-database"></i> 17+ Sources</div>
<div class="mini-stat"><i class="fa-solid fa-database"></i> 17+ Sources</div>
<div class="mini-stat"><i class="fa-solid fa-map-pin"></i> 100+ Spots</div>
<div class="mini-stat"><i class="fa-solid fa-map-pin"></i> 100+ Spots</div>
<div class="mini-stat"><i class="fa-solid fa-cloud-sun"></i> Live Weather</div>
<div class="mini-stat"><i class="fa-solid fa-cloud-sun"></i> Live Weather</div>
- <div class="mini-stat"><i class="fa-solid fa-globe-americas"></i> North America</div>
+ <div class="mini-stat"><i class="fa-solid fa-earth-americas"></i> North America</div>
</div>
</div>
</div>
</div>
<div id="results-list"></div>
<div id="results-list"></div>
@@ -224,11 +222,11 @@
@@ -224,11 +222,11 @@
<div id="map-container">
<div id="map-container">
<div id="map"></div>
<div id="map"></div>
<div class="map-ctrl-group">
<div class="map-ctrl-group">
- <button id="btn-map-locate" class="map-ctrl-btn" title="My location"><i class="fa-solid fa-street-view"></i></button>
+ <button id="btn-map-locate" class="map-ctrl-btn" title="My location"><i class="fa-solid fa-map-location-dot"></i></button>
<button id="btn-map-fullscreen" class="map-ctrl-btn" title="Fullscreen"><i class="fa-solid fa-expand"></i></button>
<button id="btn-map-fullscreen" class="map-ctrl-btn" title="Fullscreen"><i class="fa-solid fa-expand"></i></button>
</div>
</div>
<button id="btn-search-area" class="search-area-btn hidden">
<button id="btn-search-area" class="search-area-btn hidden">
- <i class="fa-solid fa-magnifying-glass-location"></i> Search This Area
+ <i class="fa-solid fa-magnifying-glass"></i> Search This Area
</button>
</button>
</div>
</div>
</main>
</main>
@@ -278,7 +276,7 @@
@@ -278,7 +276,7 @@
<button class="food-tab active" data-tab="food-banks"><i class="fa-solid fa-hand-holding-heart"></i> Food Banks</button>
<button class="food-tab active" data-tab="food-banks"><i class="fa-solid fa-hand-holding-heart"></i> Food Banks</button>
<button class="food-tab" data-tab="stores"><i class="fa-solid fa-cart-shopping"></i> Stores</button>
<button class="food-tab" data-tab="stores"><i class="fa-solid fa-cart-shopping"></i> Stores</button>
<button class="food-tab" data-tab="meal-plan"><i class="fa-solid fa-calendar-days"></i> Meal Plan</button>
<button class="food-tab" data-tab="meal-plan"><i class="fa-solid fa-calendar-days"></i> Meal Plan</button>
- <button class="food-tab" data-tab="food-db"><i class="fa-solid fa-apple-whole"></i> Food Database</button>
+ <button class="food-tab" data-tab="food-db"><i class="fa-solid fa-utensils"></i> Food Database</button>
</div>
</div>
<div id="food-banks" class="food-panel active"></div>
<div id="food-banks" class="food-panel active"></div>
<div id="stores" class="food-panel hidden"></div>
<div id="stores" class="food-panel hidden"></div>
@@ -353,15 +351,15 @@
@@ -353,15 +351,15 @@
<div id="guide-modal" class="modal hidden">
<div id="guide-modal" class="modal hidden">
<div class="modal-content modal-lg">
<div class="modal-content modal-lg">
<div class="modal-header">
<div class="modal-header">
- <h2><i class="fa-solid fa-book-skull"></i> Survival Guide</h2>
+ <h2><i class="fa-solid fa-book"></i> Survival Guide</h2>
<button class="modal-close"><i class="fa-solid fa-xmark"></i></button>
<button class="modal-close"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
<div class="modal-body">
<div class="modal-body">
<div class="guide-tabs">
<div class="guide-tabs">
<button class="guide-tab active" data-guide="stealth"><i class="fa-solid fa-eye-slash"></i> Stealth</button>
<button class="guide-tab active" data-guide="stealth"><i class="fa-solid fa-eye-slash"></i> Stealth</button>
<button class="guide-tab" data-guide="rules"><i class="fa-solid fa-gavel"></i> Rules</button>
<button class="guide-tab" data-guide="rules"><i class="fa-solid fa-gavel"></i> Rules</button>
- <button class="guide-tab" data-guide="gear"><i class="fa-solid fa-backpack"></i> Gear</button>
- <button class="guide-tab" data-guide="safety"><i class="fa-solid fa-shield-heart"></i> Safety</button>
+ <button class="guide-tab" data-guide="gear"><i class="fa-solid fa-briefcase"></i> Gear</button>
+ <button class="guide-tab" data-guide="safety"><i class="fa-solid fa-shield"></i> Safety</button>
<button class="guide-tab" data-guide="fire"><i class="fa-solid fa-fire"></i> Fire</button>
<button class="guide-tab" data-guide="fire"><i class="fa-solid fa-fire"></i> Fire</button>
<button class="guide-tab" data-guide="firstaid"><i class="fa-solid fa-kit-medical"></i> First Aid</button>
<button class="guide-tab" data-guide="firstaid"><i class="fa-solid fa-kit-medical"></i> First Aid</button>
</div>
</div>
@@ -374,7 +372,7 @@
@@ -374,7 +372,7 @@
<div id="stats-modal" class="modal hidden">
<div id="stats-modal" class="modal hidden">
<div class="modal-content modal-md">
<div class="modal-content modal-md">
<div class="modal-header">
<div class="modal-header">
- <h2><i class="fa-solid fa-chart-pie"></i> Statistics</h2>
+ <h2><i class="fa-solid fa-chart-column"></i> Statistics</h2>
<button class="modal-close"><i class="fa-solid fa-xmark"></i></button>
<button class="modal-close"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
<div class="modal-body">
<div class="modal-body">
@@ -74,7 +74,7 @@
@@ -74,7 +74,7 @@
return 'default';
return 'default';
}
}
function typeIcon(tc) {
function typeIcon(tc) {
- return { dispersed:'fa-tree',campground:'fa-campground',forest:'fa-leaf',urban:'fa-city',services:'fa-hands-helping',
+ return { dispersed:'fa-tree',campground:'fa-campground',forest:'fa-leaf',urban:'fa-city',services:'fa-handshake',
water:'fa-faucet-drip',bridge:'fa-bridge',cover:'fa-umbrella',shelter:'fa-house-chimney',restroom:'fa-restroom',
water:'fa-faucet-drip',bridge:'fa-bridge',cover:'fa-umbrella',shelter:'fa-house-chimney',restroom:'fa-restroom',
woods:'fa-tree',sketch:'fa-skull-crossbones',default:'fa-map-pin' }[tc]||'fa-map-pin';
woods:'fa-tree',sketch:'fa-skull-crossbones',default:'fa-map-pin' }[tc]||'fa-map-pin';
}
}
@@ -420,7 +420,7 @@
@@ -420,7 +420,7 @@
toast(`Found ${state.locations.length} locations near ${query}`);
toast(`Found ${state.locations.length} locations near ${query}`);
} catch (err) {
} catch (err) {
- setStatus(`<i class="fa-solid fa-exclamation-triangle"></i> ${esc(err.message)}`);
+ setStatus(`<i class="fa-solid fa-triangle-exclamation"></i> ${esc(err.message)}`);
toast(err.message, 'error');
toast(err.message, 'error');
$('#results-list').innerHTML = `<div class="muted-text"><i class="fa-solid fa-triangle-exclamation"></i> ${esc(err.message)}</div>`;
$('#results-list').innerHTML = `<div class="muted-text"><i class="fa-solid fa-triangle-exclamation"></i> ${esc(err.message)}</div>`;
} finally {
} finally {
@@ -551,12 +551,12 @@
@@ -551,12 +551,12 @@
${loc.amenities?.length?`<div class="detail-section"><h3><i class="fa-solid fa-list-check"></i> Amenities</h3><div class="result-tags">${loc.amenities.map(a=>`<span class="result-tag"><i class="fa-solid fa-check"></i> ${esc(a)}</span>`).join('')}</div></div>`:''}
${loc.amenities?.length?`<div class="detail-section"><h3><i class="fa-solid fa-list-check"></i> Amenities</h3><div class="result-tags">${loc.amenities.map(a=>`<span class="result-tag"><i class="fa-solid fa-check"></i> ${esc(a)}</span>`).join('')}</div></div>`:''}
<div class="detail-section"><h3><i class="fa-solid fa-link"></i> Actions</h3>
<div class="detail-section"><h3><i class="fa-solid fa-link"></i> Actions</h3>
<div class="detail-actions">
<div class="detail-actions">
- <a class="detail-btn" href="https://www.google.com/maps/dir/?api=1&destination=${num(loc.lat)},${num(loc.lon)}" target="_blank"><i class="fa-solid fa-diamond-turn-right"></i> Directions</a>
+ <a class="detail-btn" href="https://www.google.com/maps/dir/?api=1&destination=${num(loc.lat)},${num(loc.lon)}" target="_blank"><i class="fa-solid fa-route"></i> Directions</a>
<a class="detail-btn" href="https://www.google.com/maps/@${num(loc.lat)},${num(loc.lon)},15z" target="_blank"><i class="fa-solid fa-map"></i> Google Maps</a>
<a class="detail-btn" href="https://www.google.com/maps/@${num(loc.lat)},${num(loc.lon)},15z" target="_blank"><i class="fa-solid fa-map"></i> Google Maps</a>
<button class="detail-btn" id="btn-copy-coords"><i class="fa-solid fa-copy"></i> Copy Coords</button>
<button class="detail-btn" id="btn-copy-coords"><i class="fa-solid fa-copy"></i> Copy Coords</button>
</div>
</div>
</div>
</div>
- <div class="detail-section"><h3><i class="fa-solid fa-note-sticky"></i> Notes</h3>
+ <div class="detail-section"><h3><i class="fa-solid fa-paste"></i> Notes</h3>
<div class="detail-note-area">
<div class="detail-note-area">
<textarea id="detail-note" placeholder="Your notes...">${esc(noteText)}</textarea>
<textarea id="detail-note" placeholder="Your notes...">${esc(noteText)}</textarea>
<button class="detail-btn primary" id="btn-save-note"><i class="fa-solid fa-floppy-disk"></i> Save</button>
<button class="detail-btn primary" id="btn-save-note"><i class="fa-solid fa-floppy-disk"></i> Save</button>
@@ -618,7 +618,7 @@
@@ -618,7 +618,7 @@
if (!w || !state.searchCenter) { c.innerHTML = '<p class="muted-text">Search for a location first.</p>'; show($('#weather-modal')); return; }
if (!w || !state.searchCenter) { c.innerHTML = '<p class="muted-text">Search for a location first.</p>'; show($('#weather-modal')); return; }
let html = '<div class="detail-section">';
let html = '<div class="detail-section">';
if (w.current) {
if (w.current) {
- html += `<h3><i class="fa-solid fa-temperature-half"></i> Current Conditions</h3>`;
+ html += `<h3><i class="fa-solid fa-sun"></i> Current Conditions</h3>`;
if (w.current.temperature != null) html += `<div class="detail-info-row"><span class="label">Temperature</span><span class="value">${Math.round(w.current.temperature)}°F</span></div>`;
if (w.current.temperature != null) html += `<div class="detail-info-row"><span class="label">Temperature</span><span class="value">${Math.round(w.current.temperature)}°F</span></div>`;
if (w.current.description) html += `<div class="detail-info-row"><span class="label">Conditions</span><span class="value">${esc(w.current.description)}</span></div>`;
if (w.current.description) html += `<div class="detail-info-row"><span class="label">Conditions</span><span class="value">${esc(w.current.description)}</span></div>`;
if (w.current.windSpeed != null) html += `<div class="detail-info-row"><span class="label">Wind</span><span class="value">${w.current.windSpeed} mph</span></div>`;
if (w.current.windSpeed != null) html += `<div class="detail-info-row"><span class="label">Wind</span><span class="value">${w.current.windSpeed} mph</span></div>`;
@@ -855,7 +855,7 @@
@@ -855,7 +855,7 @@
<li><strong>City/County Parks</strong> — Camping usually prohibited.</li>
<li><strong>City/County Parks</strong> — Camping usually prohibited.</li>
<li><strong>Private Land</strong> — Need owner permission.</li>
<li><strong>Private Land</strong> — Need owner permission.</li>
<li><strong>Walmart/Rest Stops</strong> — Varies by location.</li></ul></div>`,
<li><strong>Walmart/Rest Stops</strong> — Varies by location.</li></ul></div>`,
- gear: `<div class="detail-section"><h3><i class="fa-solid fa-backpack"></i> Gear Checklist</h3><ul>
+ gear: `<div class="detail-section"><h3><i class="fa-solid fa-briefcase"></i> Gear Checklist</h3><ul>
<li><strong>Shelter:</strong> Dark bivy/tarp/tent</li>
<li><strong>Shelter:</strong> Dark bivy/tarp/tent</li>
<li><strong>Sleep:</strong> 20°F bag + insulated pad</li>
<li><strong>Sleep:</strong> 20°F bag + insulated pad</li>
<li><strong>Light:</strong> Red headlamp</li>
<li><strong>Light:</strong> Red headlamp</li>
@@ -866,7 +866,7 @@
@@ -866,7 +866,7 @@
<li><strong>First Aid:</strong> Kit + emergency blanket + whistle</li>
<li><strong>First Aid:</strong> Kit + emergency blanket + whistle</li>
<li><strong>Tools:</strong> Multi-tool, paracord 50ft, trash bags</li>
<li><strong>Tools:</strong> Multi-tool, paracord 50ft, trash bags</li>
<li><strong>Comms:</strong> Charged phone + battery. PLB for remote</li></ul></div>`,
<li><strong>Comms:</strong> Charged phone + battery. PLB for remote</li></ul></div>`,
- safety: `<div class="detail-section"><h3><i class="fa-solid fa-shield-heart"></i> Safety</h3><ul>
+ safety: `<div class="detail-section"><h3><i class="fa-solid fa-shield"></i> Safety</h3><ul>
<li><strong>Tell someone</strong> your plans</li>
<li><strong>Tell someone</strong> your plans</li>
<li><strong>Wildlife:</strong> Hang food 200ft from camp in bear country</li>
<li><strong>Wildlife:</strong> Hang food 200ft from camp in bear country</li>
<li><strong>Rivers</strong> rise fast with rain</li>
<li><strong>Rivers</strong> rise fast with rain</li>
@@ -78,12 +78,12 @@ app.use(helmet({
@@ -78,12 +78,12 @@ app.use(helmet({
contentSecurityPolicy: {
contentSecurityPolicy: {
directives: {
directives: {
defaultSrc: ["'self'"],
defaultSrc: ["'self'"],
- scriptSrc: ["'self'", "'unsafe-inline'", "unpkg.com", "cdn.jsdelivr.net", "cdnjs.cloudflare.com", "static.cloudflareinsights.com", "https://openvibe.network"],
- styleSrc: ["'self'", "'unsafe-inline'", "unpkg.com", "cdn.jsdelivr.net", "cdnjs.cloudflare.com", "fonts.googleapis.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "unpkg.com", "cdn.jsdelivr.net", "static.cloudflareinsights.com", "https://openvibe.network"],
+ styleSrc: ["'self'", "'unsafe-inline'", "unpkg.com", "cdn.jsdelivr.net", "fonts.googleapis.com"],
imgSrc: ["'self'", "data:", "blob:", "*.tile.openstreetmap.org", "*.basemaps.cartocdn.com", "server.arcgisonline.com", "image.tmdb.org", "*.wp.com"],
imgSrc: ["'self'", "data:", "blob:", "*.tile.openstreetmap.org", "*.basemaps.cartocdn.com", "server.arcgisonline.com", "image.tmdb.org", "*.wp.com"],
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
connectSrc: ["'self'", "nominatim.openstreetmap.org", "api.weather.gov", "api.open-meteo.com", "*.tile.openstreetmap.org", "*.basemaps.cartocdn.com", "server.arcgisonline.com", "https://openvibe.network", "https://openvibe.events"],
connectSrc: ["'self'", "nominatim.openstreetmap.org", "api.weather.gov", "api.open-meteo.com", "*.tile.openstreetmap.org", "*.basemaps.cartocdn.com", "server.arcgisonline.com", "https://openvibe.network", "https://openvibe.events"],
- fontSrc: ["'self'", "fonts.gstatic.com", "cdnjs.cloudflare.com", "cdn.jsdelivr.net"],
+ fontSrc: ["'self'", "fonts.gstatic.com", "cdn.jsdelivr.net"],
frameSrc: ["'none'"],
frameSrc: ["'none'"],
scriptSrcAttr: ["'unsafe-inline'"],
scriptSrcAttr: ["'unsafe-inline'"],
},
},
@@ -199,8 +199,8 @@ function getSourceDefs(lat, lon, radius) {
@@ -199,8 +199,8 @@ function getSourceDefs(lat, lon, radius) {
{ name: 'Woods', icon: 'fa-tree', fn: () => woods.findWoods(lat, lon, radius) },
{ name: 'Woods', icon: 'fa-tree', fn: () => woods.findWoods(lat, lon, radius) },
{ name: 'Waterways', icon: 'fa-water', fn: () => waterways.findWaterways(lat, lon, radius) },
{ name: 'Waterways', icon: 'fa-water', fn: () => waterways.findWaterways(lat, lon, radius) },
{ name: 'NPS', icon: 'fa-mountain-sun', fn: () => nps.search(lat, lon, radius, config.npsApiKey) },
{ name: 'NPS', icon: 'fa-mountain-sun', fn: () => nps.search(lat, lon, radius, config.npsApiKey) },
- { name: 'OpenChargeMap', icon: 'fa-charging-station', fn: () => openchargemap.search(lat, lon, radius, config.openChargeMapKey) },
- { name: 'WebScraper', icon: 'fa-spider', fn: () => scraper.search(lat, lon, radius) },
+ { name: 'OpenChargeMap', icon: 'fa-bolt', fn: () => openchargemap.search(lat, lon, radius, config.openChargeMapKey) },
+ { name: 'WebScraper', icon: 'fa-bug', fn: () => scraper.search(lat, lon, radius) },
{ name: 'Rain Cover', icon: 'fa-umbrella', fn: () => cover.findCover(lat, lon, radius) },
{ name: 'Rain Cover', icon: 'fa-umbrella', fn: () => cover.findCover(lat, lon, radius) },
{ name: 'Crime Intel', icon: 'fa-skull-crossbones', fn: () => crimedata.findSketchAreas(lat, lon, radius) },
{ name: 'Crime Intel', icon: 'fa-skull-crossbones', fn: () => crimedata.findSketchAreas(lat, lon, radius) },
{ name: 'Harm Reduction', icon: 'fa-suitcase-medical', fn: () => harmreduction.findHarmReduction(lat, lon, radius) },
{ name: 'Harm Reduction', icon: 'fa-suitcase-medical', fn: () => harmreduction.findHarmReduction(lat, lon, radius) },
@@ -21,17 +21,17 @@ const BATHROOM_TYPES = {
@@ -21,17 +21,17 @@ const BATHROOM_TYPES = {
public: { icon: 'fa-restroom', label: 'Public Restroom', color: '#3b82f6' },
public: { icon: 'fa-restroom', label: 'Public Restroom', color: '#3b82f6' },
park: { icon: 'fa-tree', label: 'Park Restroom', color: '#22c55e' },
park: { icon: 'fa-tree', label: 'Park Restroom', color: '#22c55e' },
library: { icon: 'fa-book', label: 'Library', color: '#8b5cf6' },
library: { icon: 'fa-book', label: 'Library', color: '#8b5cf6' },
- gas_station: { icon: 'fa-gas-pump', label: 'Gas Station', color: '#f59e0b' },
+ gas_station: { icon: 'fa-store', label: 'Gas Station', color: '#f59e0b' },
restaurant: { icon: 'fa-utensils', label: 'Restaurant/Café', color: '#ef4444' },
restaurant: { icon: 'fa-utensils', label: 'Restaurant/Café', color: '#ef4444' },
mall: { icon: 'fa-store', label: 'Store/Mall', color: '#ec4899' },
mall: { icon: 'fa-store', label: 'Store/Mall', color: '#ec4899' },
- transit: { icon: 'fa-bus', label: 'Transit Station', color: '#06b6d4' },
- community: { icon: 'fa-building', label: 'Community Building', color: '#6366f1' },
- shower: { icon: 'fa-shower', label: 'Shower Facility', color: '#14b8a6' },
- gym_shower: { icon: 'fa-dumbbell', label: 'Gym Shower', color: '#8b5cf6' },
+ transit: { icon: 'fa-train', label: 'Transit Station', color: '#06b6d4' },
+ community: { icon: 'fa-city', label: 'Community Building', color: '#6366f1' },
+ shower: { icon: 'fa-faucet', label: 'Shower Facility', color: '#14b8a6' },
+ gym_shower: { icon: 'fa-weight-hanging', label: 'Gym Shower', color: '#8b5cf6' },
pool_shower: { icon: 'fa-person-swimming', label: 'Pool/Sports Shower', color: '#0ea5e9' },
pool_shower: { icon: 'fa-person-swimming', label: 'Pool/Sports Shower', color: '#0ea5e9' },
hot_spring: { icon: 'fa-hot-tub-person', label: 'Hot Spring', color: '#f97316' },
hot_spring: { icon: 'fa-hot-tub-person', label: 'Hot Spring', color: '#f97316' },
water: { icon: 'fa-faucet-drip', label: 'Drinking Water', color: '#0ea5e9' },
water: { icon: 'fa-faucet-drip', label: 'Drinking Water', color: '#0ea5e9' },
- unknown: { icon: 'fa-toilet', label: 'Restroom', color: '#94a3b8' },
+ unknown: { icon: 'fa-restroom', label: 'Restroom', color: '#94a3b8' },
};
};
// ─── Classify bathroom type from tags/name ─────────────────────────
// ─── Classify bathroom type from tags/name ─────────────────────────
@@ -24,15 +24,15 @@ const { haversine, overpassQuery } = require('./utils');
@@ -24,15 +24,15 @@ const { haversine, overpassQuery } = require('./utils');
// ── Cover Type Definitions ──────────────────────────────────────────
// ── Cover Type Definitions ──────────────────────────────────────────
const COVER_TYPES = {
const COVER_TYPES = {
covered_walkway: { label: 'Covered Walkway', icon: 'fa-road', color: '#6366f1', stealth: 2 },
covered_walkway: { label: 'Covered Walkway', icon: 'fa-road', color: '#6366f1', stealth: 2 },
- arcade: { label: 'Arcade / Colonnade', icon: 'fa-archway', color: '#8b5cf6', stealth: 2 },
- canopy: { label: 'Canopy / Awning', icon: 'fa-tent-arrow-down-to-line', color: '#f59e0b', stealth: 2 },
- carport: { label: 'Carport', icon: 'fa-car-side', color: '#64748b', stealth: 3 },
+ arcade: { label: 'Arcade / Colonnade', icon: 'fa-door-open', color: '#8b5cf6', stealth: 2 },
+ canopy: { label: 'Canopy / Awning', icon: 'fa-campground', color: '#f59e0b', stealth: 2 },
+ carport: { label: 'Carport', icon: 'fa-square-parking', color: '#64748b', stealth: 3 },
porch: { label: 'Building Porch', icon: 'fa-house-chimney', color: '#f97316', stealth: 2 },
porch: { label: 'Building Porch', icon: 'fa-house-chimney', color: '#f97316', stealth: 2 },
covered_market: { label: 'Covered Market', icon: 'fa-store', color: '#ec4899', stealth: 1 },
covered_market: { label: 'Covered Market', icon: 'fa-store', color: '#ec4899', stealth: 1 },
- covered_seating: { label: 'Covered Seating', icon: 'fa-chair', color: '#14b8a6', stealth: 1 },
+ covered_seating: { label: 'Covered Seating', icon: 'fa-couch', color: '#14b8a6', stealth: 1 },
covered_platform: { label: 'Covered Platform', icon: 'fa-train', color: '#06b6d4', stealth: 1 },
covered_platform: { label: 'Covered Platform', icon: 'fa-train', color: '#06b6d4', stealth: 1 },
grandstand: { label: 'Grandstand / Bleachers', icon: 'fa-flag', color: '#3b82f6', stealth: 3 },
grandstand: { label: 'Grandstand / Bleachers', icon: 'fa-flag', color: '#3b82f6', stealth: 3 },
- loading_dock: { label: 'Loading Dock / Bay', icon: 'fa-warehouse', color: '#94a3b8', stealth: 3 },
+ loading_dock: { label: 'Loading Dock / Bay', icon: 'fa-box-open', color: '#94a3b8', stealth: 3 },
covered_area: { label: 'Covered Area', icon: 'fa-cloud-rain', color: '#22c55e', stealth: 2 },
covered_area: { label: 'Covered Area', icon: 'fa-cloud-rain', color: '#22c55e', stealth: 2 },
curated: { label: 'Known Cover Spot', icon: 'fa-umbrella', color: '#a855f7', stealth: 3 },
curated: { label: 'Known Cover Spot', icon: 'fa-umbrella', color: '#a855f7', stealth: 3 },
};
};
@@ -18,29 +18,29 @@ const { haversine, overpassQuery } = require('./utils');
@@ -18,29 +18,29 @@ const { haversine, overpassQuery } = require('./utils');
// Higher weight = stronger signal of "sketchy" area
// Higher weight = stronger signal of "sketchy" area
const GRIT_INDICATORS = {
const GRIT_INDICATORS = {
// Nightlife / vice
// Nightlife / vice
- bar: { query: '["amenity"="bar"]', weight: 2, label: 'Bar', icon: 'fa-martini-glass' },
+ bar: { query: '["amenity"="bar"]', weight: 2, label: 'Bar', icon: 'fa-mug-hot' },
nightclub: { query: '["amenity"="nightclub"]', weight: 3, label: 'Nightclub', icon: 'fa-music' },
nightclub: { query: '["amenity"="nightclub"]', weight: 3, label: 'Nightclub', icon: 'fa-music' },
- pub: { query: '["amenity"="pub"]', weight: 1, label: 'Pub', icon: 'fa-beer-mug-empty' },
+ pub: { query: '["amenity"="pub"]', weight: 1, label: 'Pub', icon: 'fa-mug-saucer' },
strip_club: { query: '["amenity"="stripclub"]', weight: 4, label: 'Strip Club', icon: 'fa-mask' },
strip_club: { query: '["amenity"="stripclub"]', weight: 4, label: 'Strip Club', icon: 'fa-mask' },
casino: { query: '["amenity"="casino"]', weight: 3, label: 'Casino', icon: 'fa-dice' },
casino: { query: '["amenity"="casino"]', weight: 3, label: 'Casino', icon: 'fa-dice' },
adult_shop: { query: '["shop"="erotic"]', weight: 3, label: 'Adult Shop', icon: 'fa-store' },
adult_shop: { query: '["shop"="erotic"]', weight: 3, label: 'Adult Shop', icon: 'fa-store' },
// Commerce grit
// Commerce grit
- pawn_shop: { query: '["shop"="pawnbroker"]', weight: 3, label: 'Pawn Shop', icon: 'fa-ring' },
+ pawn_shop: { query: '["shop"="pawnbroker"]', weight: 3, label: 'Pawn Shop', icon: 'fa-gem' },
payday_loan: { query: '["shop"="money_lender"]', weight: 3, label: 'Payday Lender', icon: 'fa-money-bill' },
payday_loan: { query: '["shop"="money_lender"]', weight: 3, label: 'Payday Lender', icon: 'fa-money-bill' },
- liquor_store: { query: '["shop"="alcohol"]', weight: 2, label: 'Liquor Store', icon: 'fa-wine-bottle' },
+ liquor_store: { query: '["shop"="alcohol"]', weight: 2, label: 'Liquor Store', icon: 'fa-flask' },
tattoo_shop: { query: '["shop"="tattoo"]', weight: 1, label: 'Tattoo Parlor', icon: 'fa-pen-nib' },
tattoo_shop: { query: '["shop"="tattoo"]', weight: 1, label: 'Tattoo Parlor', icon: 'fa-pen-nib' },
- vape_shop: { query: '["shop"="e-cigarette"]', weight: 1, label: 'Vape/Smoke Shop', icon: 'fa-smoking' },
- tobacco_shop: { query: '["shop"="tobacco"]', weight: 1, label: 'Tobacco Shop', icon: 'fa-smoking' },
+ vape_shop: { query: '["shop"="e-cigarette"]', weight: 1, label: 'Vape/Smoke Shop', icon: 'fa-smog' },
+ tobacco_shop: { query: '["shop"="tobacco"]', weight: 1, label: 'Tobacco Shop', icon: 'fa-smog' },
// Social services (indicates street population density)
// Social services (indicates street population density)
homeless_shelter:{ query: '["social_facility"="shelter"]["social_facility:for"~"homeless|underprivileged"]', weight: 2, label: 'Homeless Shelter', icon: 'fa-house-chimney' },
homeless_shelter:{ query: '["social_facility"="shelter"]["social_facility:for"~"homeless|underprivileged"]', weight: 2, label: 'Homeless Shelter', icon: 'fa-house-chimney' },
food_bank: { query: '["social_facility"="food_bank"]', weight: 1, label: 'Food Bank', icon: 'fa-utensils' },
food_bank: { query: '["social_facility"="food_bank"]', weight: 1, label: 'Food Bank', icon: 'fa-utensils' },
// Law enforcement (indicates high-incident areas)
// Law enforcement (indicates high-incident areas)
- police: { query: '["amenity"="police"]', weight: 1, label: 'Police Station', icon: 'fa-building-shield' },
+ police: { query: '["amenity"="police"]', weight: 1, label: 'Police Station', icon: 'fa-shield-halved' },
// Transportation (transient hubs)
// Transportation (transient hubs)
- bus_station: { query: '["amenity"="bus_station"]', weight: 1, label: 'Bus Station', icon: 'fa-bus' },
+ bus_station: { query: '["amenity"="bus_station"]', weight: 1, label: 'Bus Station', icon: 'fa-train' },
// Abandoned / sketchy structures
// Abandoned / sketchy structures
- abandoned: { query: '["abandoned"="yes"]', weight: 3, label: 'Abandoned Building', icon: 'fa-building-circle-xmark' },
- disused: { query: '["disused"="yes"]["building"]', weight: 2, label: 'Disused Building', icon: 'fa-building' },
+ abandoned: { query: '["abandoned"="yes"]', weight: 3, label: 'Abandoned Building', icon: 'fa-circle-xmark' },
+ disused: { query: '["disused"="yes"]["building"]', weight: 2, label: 'Disused Building', icon: 'fa-city' },
};
};
// ── Curated WA Sketchy / High-Activity Zones ────────────────────────
// ── Curated WA Sketchy / High-Activity Zones ────────────────────────
@@ -73,42 +73,42 @@ const STORES = {
@@ -73,42 +73,42 @@ const STORES = {
const FOOD_GROUPS = {
const FOOD_GROUPS = {
protein: {
protein: {
name: 'Protein',
name: 'Protein',
- icon: 'fa-drumstick-bite',
+ icon: 'fa-fish',
color: '#9b59b6',
color: '#9b59b6',
dailyMin: 50, // grams
dailyMin: 50, // grams
examples: 'Chicken, eggs, beans, peanut butter, canned tuna',
examples: 'Chicken, eggs, beans, peanut butter, canned tuna',
},
},
grains: {
grains: {
name: 'Grains',
name: 'Grains',
- icon: 'fa-wheat-awn',
+ icon: 'fa-seedling',
color: '#e67e22',
color: '#e67e22',
dailyMin: 170, // grams (6 oz equiv)
dailyMin: 170, // grams (6 oz equiv)
examples: 'Rice, bread, pasta, oatmeal, tortillas',
examples: 'Rice, bread, pasta, oatmeal, tortillas',
},
},
vegetables: {
vegetables: {
name: 'Vegetables',
name: 'Vegetables',
- icon: 'fa-carrot',
+ icon: 'fa-leaf',
color: '#27ae60',
color: '#27ae60',
dailyMin: 300, // grams (~2.5 cups)
dailyMin: 300, // grams (~2.5 cups)
examples: 'Carrots, potatoes, canned corn, frozen mixed veggies',
examples: 'Carrots, potatoes, canned corn, frozen mixed veggies',
},
},
fruits: {
fruits: {
name: 'Fruits',
name: 'Fruits',
- icon: 'fa-apple-whole',
+ icon: 'fa-utensils',
color: '#e74c3c',
color: '#e74c3c',
dailyMin: 200, // grams (~2 cups)
dailyMin: 200, // grams (~2 cups)
examples: 'Bananas, apples, oranges, canned fruit',
examples: 'Bananas, apples, oranges, canned fruit',
},
},
dairy: {
dairy: {
name: 'Dairy',
name: 'Dairy',
- icon: 'fa-glass-water',
+ icon: 'fa-mug-saucer',
color: '#3498db',
color: '#3498db',
dailyMin: 720, // ml (~3 cups)
dailyMin: 720, // ml (~3 cups)
examples: 'Milk, cheese, yogurt',
examples: 'Milk, cheese, yogurt',
},
},
fats: {
fats: {
name: 'Fats & Oils',
name: 'Fats & Oils',
- icon: 'fa-oil-can',
+ icon: 'fa-droplet',
color: '#f1c40f',
color: '#f1c40f',
dailyMin: 25, // grams
dailyMin: 25, // grams
examples: 'Cooking oil, butter, peanut butter, avocado',
examples: 'Cooking oil, butter, peanut butter, avocado',
@@ -864,9 +864,9 @@ function formatOSMAddress(tags) {
@@ -864,9 +864,9 @@ function formatOSMAddress(tags) {
// ─── Find Food Banks, Soup Kitchens, Community Fridges ─────────────
// ─── Find Food Banks, Soup Kitchens, Community Fridges ─────────────
const FOOD_BANK_TYPES = {
const FOOD_BANK_TYPES = {
- food_bank: { label: 'Food Bank', icon: 'fa-boxes-stacked', color: '#ef4444' },
- soup_kitchen: { label: 'Soup Kitchen', icon: 'fa-bowl-food', color: '#f97316' },
- food_sharing: { label: 'Community Fridge', icon: 'fa-temperature-low', color: '#06b6d4' },
+ food_bank: { label: 'Food Bank', icon: 'fa-cubes', color: '#ef4444' },
+ soup_kitchen: { label: 'Soup Kitchen', icon: 'fa-utensils', color: '#f97316' },
+ food_sharing: { label: 'Community Fridge', icon: 'fa-snowflake', color: '#06b6d4' },
give_box: { label: 'Free Pantry Box', icon: 'fa-box-open', color: '#8b5cf6' },
give_box: { label: 'Free Pantry Box', icon: 'fa-box-open', color: '#8b5cf6' },
};
};
@@ -18,18 +18,18 @@ const { haversine, overpassQuery } = require('./utils');
@@ -18,18 +18,18 @@ const { haversine, overpassQuery } = require('./utils');
// HARM REDUCTION CATEGORIES
// HARM REDUCTION CATEGORIES
// ═══════════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════════
const HR_TYPES = {
const HR_TYPES = {
- needle_exchange: { label: 'Syringe Exchange', icon: 'fa-syringe', color: '#dc2626' },
+ needle_exchange: { label: 'Syringe Exchange', icon: 'fa-briefcase-medical', color: '#dc2626' },
condom_dist: { label: 'Condom Distribution', icon: 'fa-heart', color: '#ec4899' },
condom_dist: { label: 'Condom Distribution', icon: 'fa-heart', color: '#ec4899' },
naloxone: { label: 'Naloxone / Narcan', icon: 'fa-kit-medical', color: '#f97316' },
naloxone: { label: 'Naloxone / Narcan', icon: 'fa-kit-medical', color: '#f97316' },
harm_reduction: { label: 'Harm Reduction Center', icon: 'fa-hand-holding-heart', color: '#8b5cf6' },
harm_reduction: { label: 'Harm Reduction Center', icon: 'fa-hand-holding-heart', color: '#8b5cf6' },
addiction_counsel: { label: 'Addiction Counselling', icon: 'fa-comments', color: '#3b82f6' },
addiction_counsel: { label: 'Addiction Counselling', icon: 'fa-comments', color: '#3b82f6' },
- hiv_testing: { label: 'HIV / STI Testing', icon: 'fa-vial', color: '#ef4444' },
- sexual_health: { label: 'Sexual Health Clinic', icon: 'fa-stethoscope', color: '#0891b2' },
- family_planning: { label: 'Family Planning', icon: 'fa-baby', color: '#a855f7' },
- vending_health: { label: 'Health Vending Machine', icon: 'fa-cash-register', color: '#22c55e' },
+ hiv_testing: { label: 'HIV / STI Testing', icon: 'fa-flask-vial', color: '#ef4444' },
+ sexual_health: { label: 'Sexual Health Clinic', icon: 'fa-kit-medical', color: '#0891b2' },
+ family_planning: { label: 'Family Planning', icon: 'fa-child', color: '#a855f7' },
+ vending_health: { label: 'Health Vending Machine', icon: 'fa-money-bill', color: '#22c55e' },
outreach: { label: 'Outreach Program', icon: 'fa-people-arrows', color: '#10b981' },
outreach: { label: 'Outreach Program', icon: 'fa-people-arrows', color: '#10b981' },
- rehab: { label: 'Rehab / Recovery', icon: 'fa-house-medical', color: '#6366f1' },
- social_health: { label: 'Community Health Services', icon: 'fa-hospital', color: '#0ea5e9' },
+ rehab: { label: 'Rehab / Recovery', icon: 'fa-house', color: '#6366f1' },
+ social_health: { label: 'Community Health Services', icon: 'fa-kit-medical', color: '#0ea5e9' },
};
};
// ═══════════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════════
@@ -15,16 +15,16 @@ const CATEGORIES = {
@@ -15,16 +15,16 @@ const CATEGORIES = {
'Informal Campsite': { type: 'Informal Camp', stealth: 4, icon: 'fa-campground' },
'Informal Campsite': { type: 'Informal Camp', stealth: 4, icon: 'fa-campground' },
'Established Campground': { type: 'Campground', stealth: 2, icon: 'fa-campground' },
'Established Campground': { type: 'Campground', stealth: 2, icon: 'fa-campground' },
'Overnight Parking': { type: 'Overnight Parking', stealth: 3, icon: 'fa-parking' },
'Overnight Parking': { type: 'Overnight Parking', stealth: 3, icon: 'fa-parking' },
- 'Rest Area': { type: 'Rest Area', stealth: 2, icon: 'fa-bed' },
- 'Gas Station': { type: 'Services', stealth: 1, icon: 'fa-gas-pump' },
+ 'Rest Area': { type: 'Rest Area', stealth: 2, icon: 'fa-moon' },
+ 'Gas Station': { type: 'Services', stealth: 1, icon: 'fa-store' },
'Water': { type: 'Water Source', stealth: 0, icon: 'fa-faucet-drip' },
'Water': { type: 'Water Source', stealth: 0, icon: 'fa-faucet-drip' },
- 'Dump Station': { type: 'Services', stealth: 0, icon: 'fa-dumpster' },
- 'Propane': { type: 'Services', stealth: 0, icon: 'fa-fire-flame-simple' },
+ 'Dump Station': { type: 'Services', stealth: 0, icon: 'fa-trash' },
+ 'Propane': { type: 'Services', stealth: 0, icon: 'fa-fire' },
'Mechanic': { type: 'Services', stealth: 0, icon: 'fa-wrench' },
'Mechanic': { type: 'Services', stealth: 0, icon: 'fa-wrench' },
'Wifi': { type: 'Services', stealth: 0, icon: 'fa-wifi' },
'Wifi': { type: 'Services', stealth: 0, icon: 'fa-wifi' },
'Laundry': { type: 'Services', stealth: 0, icon: 'fa-shirt' },
'Laundry': { type: 'Services', stealth: 0, icon: 'fa-shirt' },
- 'Hostel': { type: 'Shelter', stealth: 1, icon: 'fa-bed' },
- 'Hotel': { type: 'Shelter', stealth: 1, icon: 'fa-hotel' },
+ 'Hostel': { type: 'Shelter', stealth: 1, icon: 'fa-moon' },
+ 'Hotel': { type: 'Shelter', stealth: 1, icon: 'fa-couch' },
};
};
/**
/**
@@ -89,7 +89,7 @@ async function search(lat, lon, radiusMiles, apiKey) {
@@ -89,7 +89,7 @@ async function search(lat, lon, radiusMiles, apiKey) {
distanceMiles: Math.round(dist * 10) / 10,
distanceMiles: Math.round(dist * 10) / 10,
type: 'EV Charging',
type: 'EV Charging',
source: 'OpenChargeMap',
source: 'OpenChargeMap',
- sourceIcon: 'fa-charging-station',
+ sourceIcon: 'fa-bolt',
reservable: false,
reservable: false,
url: `https://openchargemap.org/site/poi/details/${s.ID}`,
url: `https://openchargemap.org/site/poi/details/${s.ID}`,
fee: isFree ? 'Free' : (s.UsageCost || 'Check station'),
fee: isFree ? 'Free' : (s.UsageCost || 'Check station'),
@@ -18,9 +18,9 @@ const { haversine, OVERPASS_URL, overpassQuery } = require('./utils');
@@ -18,9 +18,9 @@ const { haversine, OVERPASS_URL, overpassQuery } = require('./utils');
// ═══════════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════════
const RESOURCE_TYPES = {
const RESOURCE_TYPES = {
library: { label: 'Library', icon: 'fa-book', color: '#8b5cf6' },
library: { label: 'Library', icon: 'fa-book', color: '#8b5cf6' },
- shower: { label: 'Public Shower', icon: 'fa-shower', color: '#06b6d4' },
+ shower: { label: 'Public Shower', icon: 'fa-faucet', color: '#06b6d4' },
laundry: { label: 'Laundromat', icon: 'fa-shirt', color: '#f59e0b' },
laundry: { label: 'Laundromat', icon: 'fa-shirt', color: '#f59e0b' },
- community: { label: 'Community Center', icon: 'fa-people-roof', color: '#ec4899' },
+ community: { label: 'Community Center', icon: 'fa-people-group', color: '#ec4899' },
water: { label: 'Drinking Water', icon: 'fa-faucet-drip', color: '#3b82f6' },
water: { label: 'Drinking Water', icon: 'fa-faucet-drip', color: '#3b82f6' },
food_bank: { label: 'Food Bank', icon: 'fa-utensils', color: '#ef4444' },
food_bank: { label: 'Food Bank', icon: 'fa-utensils', color: '#ef4444' },
social: { label: 'Social Services', icon: 'fa-hand-holding-heart', color: '#10b981' },
social: { label: 'Social Services', icon: 'fa-hand-holding-heart', color: '#10b981' },
@@ -30,19 +30,19 @@ const RESOURCE_TYPES = {
@@ -30,19 +30,19 @@ const RESOURCE_TYPES = {
bottle_return: { label: 'Bottle Return', icon: 'fa-recycle', color: '#22c55e' },
bottle_return: { label: 'Bottle Return', icon: 'fa-recycle', color: '#22c55e' },
clinic: { label: 'Free Clinic', icon: 'fa-kit-medical', color: '#dc2626' },
clinic: { label: 'Free Clinic', icon: 'fa-kit-medical', color: '#dc2626' },
homeless_shelter: { label: 'Homeless Shelter', icon: 'fa-house-chimney', color: '#7c3aed' },
homeless_shelter: { label: 'Homeless Shelter', icon: 'fa-house-chimney', color: '#7c3aed' },
- hospital: { label: 'Hospital / ER', icon: 'fa-hospital', color: '#be123c' },
- pharmacy: { label: 'Pharmacy', icon: 'fa-prescription', color: '#0891b2' },
+ hospital: { label: 'Hospital / ER', icon: 'fa-kit-medical', color: '#be123c' },
+ pharmacy: { label: 'Pharmacy', icon: 'fa-suitcase-medical', color: '#0891b2' },
post_office: { label: 'Post Office', icon: 'fa-envelope', color: '#4338ca' },
post_office: { label: 'Post Office', icon: 'fa-envelope', color: '#4338ca' },
thrift_store: { label: 'Thrift / Free Store', icon: 'fa-shirt', color: '#a16207' },
thrift_store: { label: 'Thrift / Free Store', icon: 'fa-shirt', color: '#a16207' },
public_bookcase: { label: 'Little Free Library', icon: 'fa-book-open', color: '#92400e' },
public_bookcase: { label: 'Little Free Library', icon: 'fa-book-open', color: '#92400e' },
water_point: { label: 'Water Point (Tank)', icon: 'fa-faucet', color: '#0284c7' },
water_point: { label: 'Water Point (Tank)', icon: 'fa-faucet', color: '#0284c7' },
- place_of_worship: { label: 'Church / Worship', icon: 'fa-church', color: '#6366f1' },
- bus_station: { label: 'Bus / Transit Stop', icon: 'fa-bus', color: '#0ea5e9' },
- gas_station: { label: 'Gas Station', icon: 'fa-gas-pump', color: '#f43f5e' },
- clothing_bank: { label: 'Clothing Bank', icon: 'fa-vest', color: '#d946ef' },
+ place_of_worship: { label: 'Church / Worship', icon: 'fa-house-chimney', color: '#6366f1' },
+ bus_station: { label: 'Bus / Transit Stop', icon: 'fa-train', color: '#0ea5e9' },
+ gas_station: { label: 'Gas Station', icon: 'fa-store', color: '#f43f5e' },
+ clothing_bank: { label: 'Clothing Bank', icon: 'fa-shirt', color: '#d946ef' },
day_shelter: { label: 'Day Shelter / Drop-In',icon: 'fa-sun', color: '#fb923c' },
day_shelter: { label: 'Day Shelter / Drop-In',icon: 'fa-sun', color: '#fb923c' },
pet_friendly: { label: 'Pet-Friendly Shelter', icon: 'fa-paw', color: '#84cc16' },
pet_friendly: { label: 'Pet-Friendly Shelter', icon: 'fa-paw', color: '#84cc16' },
- warming_center: { label: 'Warming / Cooling', icon: 'fa-temperature-half', color: '#ef4444' },
+ warming_center: { label: 'Warming / Cooling', icon: 'fa-sun', color: '#ef4444' },
};
};
// ═══════════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════════
@@ -195,11 +195,11 @@ out center body;`;
@@ -195,11 +195,11 @@ out center body;`;
stealthRating = 2;
stealthRating = 2;
} else if (tags.amenity === 'rest_area' || tags.highway === 'rest_area') {
} else if (tags.amenity === 'rest_area' || tags.highway === 'rest_area') {
type = 'Rest Area';
type = 'Rest Area';
- icon = 'fa-bed';
+ icon = 'fa-moon';
stealthRating = 3;
stealthRating = 3;
} else if (tags.tourism === 'caravan_site') {
} else if (tags.tourism === 'caravan_site') {
type = 'RV/Caravan Site';
type = 'RV/Caravan Site';
- icon = 'fa-caravan';
+ icon = 'fa-car';
stealthRating = 3;
stealthRating = 3;
}
}
@@ -50,8 +50,8 @@ const locations = [
@@ -50,8 +50,8 @@ const locations = [
{ name: 'Stillaguamish River – South Fork at Jim Creek', lat: 48.1350, lon: -121.9700, type: 'Swimming Hole', source: 'Built-in DB', sourceIcon: 'fa-person-swimming', description: 'South Fork Stillaguamish swimming access near Jim Creek area. Forest road pulloffs lead to river. Cold, clean water. Rock pools.', stealthRating: 4, fee: 'Free', amenities: ['River Swimming', 'Clean Water', 'Forest Setting', 'Rock Pools'] },
{ name: 'Stillaguamish River – South Fork at Jim Creek', lat: 48.1350, lon: -121.9700, type: 'Swimming Hole', source: 'Built-in DB', sourceIcon: 'fa-person-swimming', description: 'South Fork Stillaguamish swimming access near Jim Creek area. Forest road pulloffs lead to river. Cold, clean water. Rock pools.', stealthRating: 4, fee: 'Free', amenities: ['River Swimming', 'Clean Water', 'Forest Setting', 'Rock Pools'] },
// ── Arlington/Smokey Point Services & Resources ──
// ── Arlington/Smokey Point Services & Resources ──
{ name: 'Arlington Community Food Bank', lat: 48.1684, lon: -122.1465, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-utensils', description: 'Free food bank at 19118 63rd Ave NE. Open Tue/Wed/Fri 12:30-3:30 PM. Fresh produce, canned goods, bread, dairy. No questions asked. Serves North Snohomish County.', stealthRating: 0, fee: 'Free', amenities: ['Food Bank', 'Fresh Produce', 'No ID Required'] },
{ name: 'Arlington Community Food Bank', lat: 48.1684, lon: -122.1465, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-utensils', description: 'Free food bank at 19118 63rd Ave NE. Open Tue/Wed/Fri 12:30-3:30 PM. Fresh produce, canned goods, bread, dairy. No questions asked. Serves North Snohomish County.', stealthRating: 0, fee: 'Free', amenities: ['Food Bank', 'Fresh Produce', 'No ID Required'] },
- { name: 'Village Community Services – Smokey Point', lat: 48.1547, lon: -122.1864, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-hands-helping', description: 'Social services at 3210 Smokey Point Dr (Royal Pacific Bldg). Emergency assistance, food, clothing, case management. Mon-Fri 8AM-5PM. Serves homeless individuals.', stealthRating: 0, fee: 'Free', amenities: ['Emergency Aid', 'Food', 'Clothing', 'Case Management'] },
- { name: 'VOA Arlington Resource Center', lat: 48.1951, lon: -122.1269, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-hands-helping', description: 'Volunteers of America resource center at 103 E 3rd St in downtown Arlington. Housing assistance, case management, community resources. Mon-Fri 9AM-5PM.', stealthRating: 0, fee: 'Free', amenities: ['Housing Help', 'Case Management', 'Community Resources'] },
+ { name: 'Village Community Services – Smokey Point', lat: 48.1547, lon: -122.1864, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-handshake', description: 'Social services at 3210 Smokey Point Dr (Royal Pacific Bldg). Emergency assistance, food, clothing, case management. Mon-Fri 8AM-5PM. Serves homeless individuals.', stealthRating: 0, fee: 'Free', amenities: ['Emergency Aid', 'Food', 'Clothing', 'Case Management'] },
+ { name: 'VOA Arlington Resource Center', lat: 48.1951, lon: -122.1269, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-handshake', description: 'Volunteers of America resource center at 103 E 3rd St in downtown Arlington. Housing assistance, case management, community resources. Mon-Fri 9AM-5PM.', stealthRating: 0, fee: 'Free', amenities: ['Housing Help', 'Case Management', 'Community Resources'] },
{ name: 'Salt of the Earth Food Blessing', lat: 48.1548, lon: -122.1856, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-utensils', description: 'Free food distribution at 3310 Smokey Point Dr. Faith-based food blessing program. Open select days. No ID or proof of need required.', stealthRating: 0, fee: 'Free', amenities: ['Food Distribution', 'No ID Required'] },
{ name: 'Salt of the Earth Food Blessing', lat: 48.1548, lon: -122.1856, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-utensils', description: 'Free food distribution at 3310 Smokey Point Dr. Faith-based food blessing program. Open select days. No ID or proof of need required.', stealthRating: 0, fee: 'Free', amenities: ['Food Distribution', 'No ID Required'] },
{ name: 'Walmart Supercenter – Smokey Point', lat: 48.1507, lon: -122.1746, type: 'Urban Stealth', source: 'Built-in DB', sourceIcon: 'fa-map-pin', description: 'Walmart at 4010 172nd St NE near Smokey Point. 24-hour access. Restrooms, car camping in lot (ask mgmt). Hot food deli, pharmacy, supplies.', stealthRating: 2, fee: 'Free', amenities: ['24hr Restrooms', 'Car Camping', 'Supplies', 'Pharmacy', 'Hot Food'] },
{ name: 'Walmart Supercenter – Smokey Point', lat: 48.1507, lon: -122.1746, type: 'Urban Stealth', source: 'Built-in DB', sourceIcon: 'fa-map-pin', description: 'Walmart at 4010 172nd St NE near Smokey Point. 24-hour access. Restrooms, car camping in lot (ask mgmt). Hot food deli, pharmacy, supplies.', stealthRating: 2, fee: 'Free', amenities: ['24hr Restrooms', 'Car Camping', 'Supplies', 'Pharmacy', 'Hot Food'] },
{ name: 'Grocery Outlet – Arlington', lat: 48.2008, lon: -122.1259, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-map-pin', description: 'Discount grocery at 123 E Burke Ave in downtown Arlington. Cheap food, produce, essentials. Walking distance from Centennial Trail.', stealthRating: 0, fee: 'Discount', amenities: ['Cheap Food', 'Downtown', 'Near Trail'] },
{ name: 'Grocery Outlet – Arlington', lat: 48.2008, lon: -122.1259, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-map-pin', description: 'Discount grocery at 123 E Burke Ave in downtown Arlington. Cheap food, produce, essentials. Walking distance from Centennial Trail.', stealthRating: 0, fee: 'Discount', amenities: ['Cheap Food', 'Downtown', 'Near Trail'] },
@@ -141,7 +141,7 @@ const locations = [
@@ -141,7 +141,7 @@ const locations = [
{ name: 'Centennial Trail Bridge – Stillaguamish', lat: 48.1975, lon: -122.1200, type: 'Bridge', source: 'Built-in DB', sourceIcon: 'fa-bridge', description: 'RIVER BRIDGE: Centennial Trail pedestrian/bike bridge over Stillaguamish River near downtown Arlington. Great views. River access from trail.', stealthRating: 2, fee: 'Free', amenities: ['Pedestrian Bridge', 'Trail Access', 'River Views'] },
{ name: 'Centennial Trail Bridge – Stillaguamish', lat: 48.1975, lon: -122.1200, type: 'Bridge', source: 'Built-in DB', sourceIcon: 'fa-bridge', description: 'RIVER BRIDGE: Centennial Trail pedestrian/bike bridge over Stillaguamish River near downtown Arlington. Great views. River access from trail.', stealthRating: 2, fee: 'Free', amenities: ['Pedestrian Bridge', 'Trail Access', 'River Views'] },
// ── Arlington/Smokey Point – SHOWERS ──
// ── Arlington/Smokey Point – SHOWERS ──
- { name: 'Planet Fitness – Smokey Point', lat: 48.1510, lon: -122.1793, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-shower', description: 'Planet Fitness gym at 3713 172nd St NE. $10/month membership gives 24/7 access to showers, lockers, WiFi. Clean facilities. Day pass sometimes available.', stealthRating: 0, fee: '$10/mo', amenities: ['24/7 Showers', 'WiFi', 'Lockers', 'Climate Control'] },
+ { name: 'Planet Fitness – Smokey Point', lat: 48.1510, lon: -122.1793, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-faucet', description: 'Planet Fitness gym at 3713 172nd St NE. $10/month membership gives 24/7 access to showers, lockers, WiFi. Clean facilities. Day pass sometimes available.', stealthRating: 0, fee: '$10/mo', amenities: ['24/7 Showers', 'WiFi', 'Lockers', 'Climate Control'] },
{ name: 'Cascade Valley Hospital – ER Lobby', lat: 48.1967, lon: -122.1192, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-map-pin', description: 'Cascade Valley Hospital ER lobby is 24/7 accessible. Warm waiting area, restrooms, water fountain. Social workers available during business hours for housing/resource referrals.', stealthRating: 0, fee: 'Free lobby', amenities: ['24hr Lobby', 'Restrooms', 'Water', 'Warm', 'Social Worker'] },
{ name: 'Cascade Valley Hospital – ER Lobby', lat: 48.1967, lon: -122.1192, type: 'Services', source: 'Built-in DB', sourceIcon: 'fa-map-pin', description: 'Cascade Valley Hospital ER lobby is 24/7 accessible. Warm waiting area, restrooms, water fountain. Social workers available during business hours for housing/resource referrals.', stealthRating: 0, fee: 'Free lobby', amenities: ['24hr Lobby', 'Restrooms', 'Water', 'Warm', 'Social Worker'] },
// ── Arlington – FOREST AREAS & NATURE (near 43rd Ave NE) ──
// ── Arlington – FOREST AREAS & NATURE (near 43rd Ave NE) ──
@@ -42,7 +42,7 @@ async function getBulkElevation(coords) {
@@ -42,7 +42,7 @@ async function getBulkElevation(coords) {
function classifyElevation(elev) {
function classifyElevation(elev) {
if (elev === null) return { class: 'unknown', label: 'Unknown', icon: 'fa-question' };
if (elev === null) return { class: 'unknown', label: 'Unknown', icon: 'fa-question' };
if (elev < 100) return { class: 'lowland', label: 'Lowland/Valley', icon: 'fa-water', note: 'Near sea level. May flood in winter.' };
if (elev < 100) return { class: 'lowland', label: 'Lowland/Valley', icon: 'fa-water', note: 'Near sea level. May flood in winter.' };
- if (elev < 500) return { class: 'foothills', label: 'Foothills', icon: 'fa-hill-rockslide', note: 'Moderate elevation. Good year-round camping.' };
+ if (elev < 500) return { class: 'foothills', label: 'Foothills', icon: 'fa-mountain', note: 'Moderate elevation. Good year-round camping.' };
if (elev < 1500) return { class: 'mountain', label: 'Mountain', icon: 'fa-mountain', note: 'Mountain terrain. Snow possible Oct-May.' };
if (elev < 1500) return { class: 'mountain', label: 'Mountain', icon: 'fa-mountain', note: 'Mountain terrain. Snow possible Oct-May.' };
if (elev < 2500) return { class: 'alpine', label: 'High Mountain', icon: 'fa-mountain-sun', note: 'High altitude. Snow likely Nov-Jun. Be prepared for weather.' };
if (elev < 2500) return { class: 'alpine', label: 'High Mountain', icon: 'fa-mountain-sun', note: 'High altitude. Snow likely Nov-Jun. Be prepared for weather.' };
return { class: 'alpine-extreme', label: 'Alpine/Extreme', icon: 'fa-mountain-sun', note: 'Very high altitude. Extreme conditions. Summer access only.' };
return { class: 'alpine-extreme', label: 'Alpine/Extreme', icon: 'fa-mountain-sun', note: 'Very high altitude. Extreme conditions. Summer access only.' };
@@ -28,14 +28,14 @@ const ACTIVITY_MAP = {
@@ -28,14 +28,14 @@ const ACTIVITY_MAP = {
'Picnicking': { type: 'USFS Picnic Area', icon: 'fa-utensils', stealth: 2 },
'Picnicking': { type: 'USFS Picnic Area', icon: 'fa-utensils', stealth: 2 },
'Fishing': { type: 'Fishing Access', icon: 'fa-fish', stealth: 3 },
'Fishing': { type: 'Fishing Access', icon: 'fa-fish', stealth: 3 },
'Swimming': { type: 'Swimming Area', icon: 'fa-water', stealth: 2 },
'Swimming': { type: 'Swimming Area', icon: 'fa-water', stealth: 2 },
- 'Boating': { type: 'Boat Launch', icon: 'fa-sailboat', stealth: 2 },
- 'Mountain Biking': { type: 'MTB Trailhead', icon: 'fa-bicycle', stealth: 3 },
+ 'Boating': { type: 'Boat Launch', icon: 'fa-water', stealth: 2 },
+ 'Mountain Biking': { type: 'MTB Trailhead', icon: 'fa-road', stealth: 3 },
'Winter Sports': { type: 'Winter Sports Area', icon: 'fa-snowflake', stealth: 2 },
'Winter Sports': { type: 'Winter Sports Area', icon: 'fa-snowflake', stealth: 2 },
- 'OHV/Off-Highway Driving': { type: 'OHV Area', icon: 'fa-truck-monster', stealth: 4 },
- 'Horseback Riding': { type: 'Equestrian Trailhead', icon: 'fa-horse', stealth: 3 },
- 'Wildlife Viewing': { type: 'Wildlife Viewing', icon: 'fa-binoculars', stealth: 3 },
+ 'OHV/Off-Highway Driving': { type: 'OHV Area', icon: 'fa-car', stealth: 4 },
+ 'Horseback Riding': { type: 'Equestrian Trailhead', icon: 'fa-paw', stealth: 3 },
+ 'Wildlife Viewing': { type: 'Wildlife Viewing', icon: 'fa-eye', stealth: 3 },
'Visitor Centers': { type: 'Visitor Center', icon: 'fa-info-circle', stealth: 1 },
'Visitor Centers': { type: 'Visitor Center', icon: 'fa-info-circle', stealth: 1 },
- 'Nature Viewing': { type: 'Nature Viewing', icon: 'fa-binoculars', stealth: 3 },
+ 'Nature Viewing': { type: 'Nature Viewing', icon: 'fa-eye', stealth: 3 },
'Interpretive Programs': { type: 'Interpretive Site', icon: 'fa-book-open', stealth: 1 },
'Interpretive Programs': { type: 'Interpretive Site', icon: 'fa-book-open', stealth: 1 },
'Climbing': { type: 'Climbing Area', icon: 'fa-mountain', stealth: 4 },
'Climbing': { type: 'Climbing Area', icon: 'fa-mountain', stealth: 4 },
'Water Sports': { type: 'Water Sports', icon: 'fa-water', stealth: 2 },
'Water Sports': { type: 'Water Sports', icon: 'fa-water', stealth: 2 },
@@ -46,8 +46,8 @@ const WATERWAY_TYPES = {
@@ -46,8 +46,8 @@ const WATERWAY_TYPES = {
ford: { label: 'Ford / River Crossing',icon: 'fa-road', color: '#0891b2', stealth: 4 },
ford: { label: 'Ford / River Crossing',icon: 'fa-road', color: '#0891b2', stealth: 4 },
fishing: { label: 'Fishing Area', icon: 'fa-fish', color: '#059669', stealth: 3 },
fishing: { label: 'Fishing Area', icon: 'fa-fish', color: '#059669', stealth: 3 },
swimming: { label: 'Swimming Area', icon: 'fa-person-swimming', color: '#14b8a6', stealth: 3 },
swimming: { label: 'Swimming Area', icon: 'fa-person-swimming', color: '#14b8a6', stealth: 3 },
- boat_launch: { label: 'Boat Launch / Slipway',icon: 'fa-ship', color: '#0d9488', stealth: 2 },
- pier: { label: 'Pier / Dock', icon: 'fa-anchor', color: '#64748b', stealth: 2 },
+ boat_launch: { label: 'Boat Launch / Slipway',icon: 'fa-water', color: '#0d9488', stealth: 2 },
+ pier: { label: 'Pier / Dock', icon: 'fa-map-pin', color: '#64748b', stealth: 2 },
wetland: { label: 'Wetland', icon: 'fa-water', color: '#84cc16', stealth: 3 },
wetland: { label: 'Wetland', icon: 'fa-water', color: '#84cc16', stealth: 3 },
river_area: { label: 'River Area', icon: 'fa-water', color: '#2563eb', stealth: 4 },
river_area: { label: 'River Area', icon: 'fa-water', color: '#2563eb', stealth: 4 },
beach: { label: 'Beach', icon: 'fa-umbrella-beach', color: '#fbbf24', stealth: 3 },
beach: { label: 'Beach', icon: 'fa-umbrella-beach', color: '#fbbf24', stealth: 3 },
@@ -31,7 +31,7 @@ const WMO_CODES = {
@@ -31,7 +31,7 @@ const WMO_CODES = {
73: { description: 'Moderate snow', icon: 'fa-snowflake', cls: 'snow' },
73: { description: 'Moderate snow', icon: 'fa-snowflake', cls: 'snow' },
75: { description: 'Heavy snow', icon: 'fa-snowflake', cls: 'snow' },
75: { description: 'Heavy snow', icon: 'fa-snowflake', cls: 'snow' },
77: { description: 'Snow grains', icon: 'fa-snowflake', cls: 'snow' },
77: { description: 'Snow grains', icon: 'fa-snowflake', cls: 'snow' },
- 80: { description: 'Light showers', icon: 'fa-cloud-sun-rain', cls: 'rain' },
+ 80: { description: 'Light showers', icon: 'fa-cloud-rain', cls: 'rain' },
81: { description: 'Moderate showers', icon: 'fa-cloud-showers-heavy', cls: 'rain' },
81: { description: 'Moderate showers', icon: 'fa-cloud-showers-heavy', cls: 'rain' },
82: { description: 'Violent showers', icon: 'fa-cloud-showers-heavy', cls: 'rain' },
82: { description: 'Violent showers', icon: 'fa-cloud-showers-heavy', cls: 'rain' },
85: { description: 'Light snow showers', icon: 'fa-snowflake', cls: 'snow' },
85: { description: 'Light snow showers', icon: 'fa-snowflake', cls: 'snow' },
@@ -47,7 +47,7 @@ function decodeWMO(code, isDay) {
@@ -47,7 +47,7 @@ function decodeWMO(code, isDay) {
let icon = entry.icon;
let icon = entry.icon;
if (!isDay && icon === 'fa-sun') icon = 'fa-moon';
if (!isDay && icon === 'fa-sun') icon = 'fa-moon';
if (!isDay && icon === 'fa-cloud-sun') icon = 'fa-cloud-moon';
if (!isDay && icon === 'fa-cloud-sun') icon = 'fa-cloud-moon';
- if (!isDay && icon === 'fa-cloud-sun-rain') icon = 'fa-cloud-moon-rain';
+ if (!isDay && icon === 'fa-cloud-rain') icon = 'fa-cloud-moon';
return { ...entry, icon };
return { ...entry, icon };
}
}
@@ -18,7 +18,7 @@
@@ -18,7 +18,7 @@
"helmet": "^7.1.0",
"helmet": "^7.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
}
}
},
},
@@ -1130,9 +1130,9 @@
@@ -1130,9 +1130,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.56.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
+ "version": "4.73.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
+ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
@@ -47,7 +46,7 @@
@@ -47,7 +46,7 @@
<body>
<body>
<div class="tool-wrap" style="max-width: 1000px;">
<div class="tool-wrap" style="max-width: 1000px;">
<div class="page-header">
<div class="page-header">
- <h1><i class="fas fa-certificate"></i> Badge & Sticker Maker</h1>
+ <h1><i class="fas fa-ranking-star"></i> Badge & Sticker Maker</h1>
<p class="subtitle">Design badges, stickers, and labels with custom shapes, colors, and fonts.</p>
<p class="subtitle">Design badges, stickers, and labels with custom shapes, colors, and fonts.</p>
</div>
</div>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
@@ -42,7 +41,7 @@
@@ -42,7 +41,7 @@
<div class="btn-row">
<div class="btn-row">
<a href="https://fancy.openvibe.tools" class="btn btn-secondary"><i class="fas fa-wand-magic-sparkles"></i> Fancy Text</a>
<a href="https://fancy.openvibe.tools" class="btn btn-secondary"><i class="fas fa-wand-magic-sparkles"></i> Fancy Text</a>
<a href="https://symbols.openvibe.tools" class="btn btn-secondary"><i class="fas fa-icons"></i> Symbols</a>
<a href="https://symbols.openvibe.tools" class="btn btn-secondary"><i class="fas fa-icons"></i> Symbols</a>
- <a href="https://kaomoji.openvibe.tools" class="btn btn-secondary"><i class="fas fa-face-smile-beam"></i> Kaomoji</a>
+ <a href="https://kaomoji.openvibe.tools" class="btn btn-secondary"><i class="fas fa-face-smile"></i> Kaomoji</a>
</div>
</div>
</div>
</div>
@@ -18,14 +18,13 @@
@@ -18,14 +18,13 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
<body>
<body>
<div class="tool-wrap">
<div class="tool-wrap">
<div class="page-header">
<div class="page-header">
- <h1><i class="fas fa-braille"></i> Braille Translator</h1>
+ <h1><i class="fas fa-grip"></i> Braille Translator</h1>
<p class="subtitle">Convert text to Braille Unicode dots and decode Braille back to text.</p>
<p class="subtitle">Convert text to Braille Unicode dots and decode Braille back to text.</p>
</div>
</div>
@@ -45,7 +44,7 @@
@@ -45,7 +44,7 @@
</div>
</div>
<div class="tool-section">
<div class="tool-section">
- <h2><i class="fas fa-braille"></i> Output</h2>
+ <h2><i class="fas fa-grip"></i> Output</h2>
<div id="output" class="output-box mono" style="font-size: 1.4rem; letter-spacing: 0.2em;"></div>
<div id="output" class="output-box mono" style="font-size: 1.4rem; letter-spacing: 0.2em;"></div>
<div class="btn-row">
<div class="btn-row">
<button class="btn btn-primary" onclick="OpenVibeUI.copyText(document.getElementById('output').textContent)"><i class="fas fa-copy"></i> Copy</button>
<button class="btn btn-primary" onclick="OpenVibeUI.copyText(document.getElementById('output').textContent)"><i class="fas fa-copy"></i> Copy</button>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
@@ -18,14 +18,13 @@
@@ -18,14 +18,13 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
<body>
<body>
<div class="tool-wrap">
<div class="tool-wrap">
<div class="page-header">
<div class="page-header">
- <h1><i class="fas fa-text-slash"></i> Case Converter</h1>
+ <h1><i class="fas fa-font"></i> Case Converter</h1>
<p class="subtitle">Instantly convert text between 14 different case styles. Click any result to copy.</p>
<p class="subtitle">Instantly convert text between 14 different case styles. Click any result to copy.</p>
</div>
</div>
@@ -18,14 +18,13 @@
@@ -18,14 +18,13 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
<body>
<body>
<div class="tool-wrap">
<div class="tool-wrap">
<div class="page-header">
<div class="page-header">
- <h1><i class="fas fa-broom"></i> Text Cleaner</h1>
+ <h1><i class="fas fa-eraser"></i> Text Cleaner</h1>
<p class="subtitle">Clean up messy text with one click. Fix quotes, strip invisible characters, normalize spacing.</p>
<p class="subtitle">Clean up messy text with one click. Fix quotes, strip invisible characters, normalize spacing.</p>
</div>
</div>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
@@ -38,7 +37,7 @@
@@ -38,7 +37,7 @@
<body>
<body>
<div class="tool-wrap" style="max-width: 1000px;">
<div class="tool-wrap" style="max-width: 1000px;">
<div class="page-header">
<div class="page-header">
- <h1><i class="fas fa-code-compare"></i> Text Compare</h1>
+ <h1><i class="fas fa-arrows-left-right"></i> Text Compare</h1>
<p class="subtitle">Paste two texts side by side and see the differences highlighted line by line.</p>
<p class="subtitle">Paste two texts side by side and see the differences highlighted line by line.</p>
</div>
</div>
@@ -18,14 +18,13 @@
@@ -18,14 +18,13 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
<body>
<body>
<div class="tool-wrap">
<div class="tool-wrap">
<div class="page-header">
<div class="page-header">
- <h1><i class="fas fa-calculator"></i> Text Counter</h1>
+ <h1><i class="fas fa-table-cells"></i> Text Counter</h1>
<p class="subtitle">Real-time character, word, sentence, and paragraph counting with reading & speaking time.</p>
<p class="subtitle">Real-time character, word, sentence, and paragraph counting with reading & speaking time.</p>
</div>
</div>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
@@ -62,7 +61,7 @@
@@ -62,7 +61,7 @@
<script>
<script>
OpenVibeUI.initPage();
OpenVibeUI.initPage();
- const ICONS = { htmlEscape: 'fa-code', htmlUnescape: 'fa-code', urlEncode: 'fa-link', urlDecode: 'fa-link', base64Encode: 'fa-lock', base64Decode: 'fa-unlock', jsonStringify: 'fa-brackets-curly', jsonParse: 'fa-brackets-curly', cssEscape: 'fa-paintbrush', regexEscape: 'fa-asterisk', unicodeEscape: 'fa-globe', hexEncode: 'fa-hashtag' };
+ const ICONS = { htmlEscape: 'fa-code', htmlUnescape: 'fa-code', urlEncode: 'fa-link', urlDecode: 'fa-link', base64Encode: 'fa-lock', base64Decode: 'fa-unlock', jsonStringify: 'fa-code', jsonParse: 'fa-code', cssEscape: 'fa-paintbrush', regexEscape: 'fa-star', unicodeEscape: 'fa-globe', hexEncode: 'fa-hashtag' };
const TRANSFORMS = Object.entries(TextOpenVibeEngine.escapes).map(([id, e]) => ({ id, label: e.label, icon: ICONS[id], fn: e.fn }));
const TRANSFORMS = Object.entries(TextOpenVibeEngine.escapes).map(([id, e]) => ({ id, label: e.label, icon: ICONS[id], fn: e.fn }));
const grid = document.getElementById('grid');
const grid = document.getElementById('grid');
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
@@ -117,12 +116,16 @@
@@ -117,12 +116,16 @@
}
}
.scenario summary::-webkit-details-marker { display: none; }
.scenario summary::-webkit-details-marker { display: none; }
.scenario summary::after {
.scenario summary::after {
- content: '\f054';
- font-family: 'Font Awesome 6 Free';
- font-weight: 900;
- font-size: 0.75rem;
+ /* The OpenVibe chevron (openvibe.design/icons/chevron), painted in the text's colour. */
+ content: '';
+ width: 0.75rem;
+ height: 0.75rem;
+ flex: none;
margin-left: auto;
margin-left: auto;
color: var(--text-muted);
color: var(--text-muted);
+ background-color: currentColor;
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 6 6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;
transition: transform 0.2s;
transition: transform 0.2s;
}
}
.scenario[open] summary::after { transform: rotate(90deg); }
.scenario[open] summary::after { transform: rotate(90deg); }
@@ -187,7 +190,7 @@
@@ -187,7 +190,7 @@
<!-- ═══ TAB: KNOW YOUR RIGHTS ═══ -->
<!-- ═══ TAB: KNOW YOUR RIGHTS ═══ -->
<div class="tab-content active" id="tab-rights">
<div class="tab-content active" id="tab-rights">
<div class="tool-section">
<div class="tool-section">
- <h2><i class="fas fa-landmark"></i> Your Constitutional Rights</h2>
+ <h2><i class="fas fa-gavel"></i> Your Constitutional Rights</h2>
<p style="color:var(--text-secondary); margin-bottom:16px;">Regardless of immigration status, the U.S. Constitution guarantees these rights to all people on U.S. soil.</p>
<p style="color:var(--text-secondary); margin-bottom:16px;">Regardless of immigration status, the U.S. Constitution guarantees these rights to all people on U.S. soil.</p>
<div class="rights-grid">
<div class="rights-grid">
@@ -200,7 +203,7 @@
@@ -200,7 +203,7 @@
<p>You have the right to speak with an attorney. If detained, say: <strong>"I want to speak with a lawyer before answering any questions."</strong> You have the right to make a phone call. Immigration detainees must be given a list of free legal service providers.</p>
<p>You have the right to speak with an attorney. If detained, say: <strong>"I want to speak with a lawyer before answering any questions."</strong> You have the right to make a phone call. Immigration detainees must be given a list of free legal service providers.</p>
</div>
</div>
<div class="rights-card">
<div class="rights-card">
- <h3><i class="fas fa-house-lock"></i> Protection in Your Home</h3>
+ <h3><i class="fas fa-house"></i> Protection in Your Home</h3>
<p>ICE generally <strong>cannot enter your home without a judicial warrant</strong> signed by a judge. An ICE administrative warrant (Form I-200) does NOT give them permission to enter. You are not required to open the door. Ask them to slip the warrant under the door.</p>
<p>ICE generally <strong>cannot enter your home without a judicial warrant</strong> signed by a judge. An ICE administrative warrant (Form I-200) does NOT give them permission to enter. You are not required to open the door. Ask them to slip the warrant under the door.</p>
</div>
</div>
<div class="rights-card">
<div class="rights-card">
@@ -208,7 +211,7 @@
@@ -208,7 +211,7 @@
<p>You can refuse a search of yourself, your car, or your home. Say clearly: <strong>"I do not consent to a search."</strong> This does not guarantee they won't search, but it protects your legal rights.</p>
<p>You can refuse a search of yourself, your car, or your home. Say clearly: <strong>"I do not consent to a search."</strong> This does not guarantee they won't search, but it protects your legal rights.</p>
</div>
</div>
<div class="rights-card">
<div class="rights-card">
- <h3><i class="fas fa-file-signature"></i> Don't Sign Anything</h3>
+ <h3><i class="fas fa-file-pen"></i> Don't Sign Anything</h3>
<p><strong>Do not sign any documents</strong> without reading them carefully and speaking to a lawyer first. Signing voluntary departure papers waives your right to appear before an immigration judge and could result in a 10-year bar on re-entry.</p>
<p><strong>Do not sign any documents</strong> without reading them carefully and speaking to a lawyer first. Signing voluntary departure papers waives your right to appear before an immigration judge and could result in a 10-year bar on re-entry.</p>
</div>
</div>
<div class="rights-card">
<div class="rights-card">
@@ -266,7 +269,7 @@
@@ -266,7 +269,7 @@
</details>
</details>
<details class="scenario">
<details class="scenario">
- <summary><i class="fas fa-building"></i> ICE at Your Workplace</summary>
+ <summary><i class="fas fa-city"></i> ICE at Your Workplace</summary>
<div class="scenario-body">
<div class="scenario-body">
<ul>
<ul>
<li>Your employer should ask to see a <strong>judicial warrant</strong> before allowing ICE into non-public areas.</li>
<li>Your employer should ask to see a <strong>judicial warrant</strong> before allowing ICE into non-public areas.</li>
@@ -292,7 +295,7 @@
@@ -292,7 +295,7 @@
</details>
</details>
<details class="scenario">
<details class="scenario">
- <summary><i class="fas fa-handcuffs"></i> If You Are Detained</summary>
+ <summary><i class="fas fa-lock"></i> If You Are Detained</summary>
<div class="scenario-body">
<div class="scenario-body">
<ul>
<ul>
<li>Stay calm and repeat: <em>"I wish to remain silent. I want to speak with a lawyer."</em></li>
<li>Stay calm and repeat: <em>"I wish to remain silent. I want to speak with a lawyer."</em></li>
@@ -348,7 +351,7 @@
@@ -348,7 +351,7 @@
<!-- ═══ TAB: RESOURCES ═══ -->
<!-- ═══ TAB: RESOURCES ═══ -->
<div class="tab-content" id="tab-resources">
<div class="tab-content" id="tab-resources">
<div class="tool-section">
<div class="tool-section">
- <h2><i class="fas fa-phone-volume"></i> Emergency Hotlines</h2>
+ <h2><i class="fas fa-phone"></i> Emergency Hotlines</h2>
<div class="emergency-grid">
<div class="emergency-grid">
<div class="emergency-card">
<div class="emergency-card">
<div class="em-icon red"><i class="fas fa-phone"></i></div>
<div class="em-icon red"><i class="fas fa-phone"></i></div>
@@ -365,7 +368,7 @@
@@ -365,7 +368,7 @@
</div>
</div>
</div>
</div>
<div class="emergency-card">
<div class="emergency-card">
- <div class="em-icon green"><i class="fas fa-hands-holding-child"></i></div>
+ <div class="em-icon green"><i class="fas fa-child"></i></div>
<div class="em-info">
<div class="em-info">
<h4>RAICES Texas</h4>
<h4>RAICES Texas</h4>
<p><a href="tel:1-800-732-4237">1-800-732-4237</a> — Free legal help</p>
<p><a href="tel:1-800-732-4237">1-800-732-4237</a> — Free legal help</p>
@@ -414,9 +417,9 @@
@@ -414,9 +417,9 @@
<div class="resource-links">
<div class="resource-links">
<a class="resource-link" href="https://www.immigrationadvocates.org/legaldirectory/" target="_blank" rel="noopener"><i class="fas fa-search"></i> Find Free Legal Help Near You</a>
<a class="resource-link" href="https://www.immigrationadvocates.org/legaldirectory/" target="_blank" rel="noopener"><i class="fas fa-search"></i> Find Free Legal Help Near You</a>
<a class="resource-link" href="https://www.aclu.org/know-your-rights/immigrants-rights" target="_blank" rel="noopener"><i class="fas fa-shield-halved"></i> ACLU Know Your Rights</a>
<a class="resource-link" href="https://www.aclu.org/know-your-rights/immigrants-rights" target="_blank" rel="noopener"><i class="fas fa-shield-halved"></i> ACLU Know Your Rights</a>
- <a class="resource-link" href="https://www.raicestexas.org/know-your-rights/" target="_blank" rel="noopener"><i class="fas fa-hands-holding-child"></i> RAICES Texas</a>
+ <a class="resource-link" href="https://www.raicestexas.org/know-your-rights/" target="_blank" rel="noopener"><i class="fas fa-child"></i> RAICES Texas</a>
<a class="resource-link" href="https://unitedwedream.org/our-work/deportation-defense/know-your-rights/" target="_blank" rel="noopener"><i class="fas fa-fist-raised"></i> United We Dream</a>
<a class="resource-link" href="https://unitedwedream.org/our-work/deportation-defense/know-your-rights/" target="_blank" rel="noopener"><i class="fas fa-fist-raised"></i> United We Dream</a>
- <a class="resource-link" href="https://www.nilc.org/" target="_blank" rel="noopener"><i class="fas fa-landmark"></i> National Immigration Law Center</a>
+ <a class="resource-link" href="https://www.nilc.org/" target="_blank" rel="noopener"><i class="fas fa-gavel"></i> National Immigration Law Center</a>
<a class="resource-link" href="https://cliniclegal.org/" target="_blank" rel="noopener"><i class="fas fa-scale-balanced"></i> CLINIC Legal</a>
<a class="resource-link" href="https://cliniclegal.org/" target="_blank" rel="noopener"><i class="fas fa-scale-balanced"></i> CLINIC Legal</a>
<a class="resource-link" href="https://www.informedimmigrant.com/" target="_blank" rel="noopener"><i class="fas fa-book"></i> Informed Immigrant</a>
<a class="resource-link" href="https://www.informedimmigrant.com/" target="_blank" rel="noopener"><i class="fas fa-book"></i> Informed Immigrant</a>
</div>
</div>
@@ -426,7 +429,7 @@
@@ -426,7 +429,7 @@
<h2><i class="fas fa-mobile-screen"></i> Apps & Tools</h2>
<h2><i class="fas fa-mobile-screen"></i> Apps & Tools</h2>
<div class="rights-grid">
<div class="rights-grid">
<div class="rights-card">
<div class="rights-card">
- <h3><i class="fab fa-app-store"></i> Know Your Rights App (NAKASEC)</h3>
+ <h3><i class="fas fa-mobile-screen"></i> Know Your Rights App (NAKASEC)</h3>
<p>Mobile app for immigrants to understand their legal rights. Available on iOS.</p>
<p>Mobile app for immigrants to understand their legal rights. Available on iOS.</p>
<div class="rights-lang"><span class="lang-tag">English</span><span class="lang-tag">Español</span><span class="lang-tag">한국어</span></div>
<div class="rights-lang"><span class="lang-tag">English</span><span class="lang-tag">Español</span><span class="lang-tag">한국어</span></div>
</div>
</div>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
@@ -69,7 +68,7 @@
@@ -69,7 +68,7 @@
<span class="card-domain">ascii.openvibe.tools</span>
<span class="card-domain">ascii.openvibe.tools</span>
</a>
</a>
<a href="https://case.openvibe.tools" class="tool-card">
<a href="https://case.openvibe.tools" class="tool-card">
- <div class="card-icon"><i class="fas fa-text-slash"></i></div>
+ <div class="card-icon"><i class="fas fa-font"></i></div>
<h3>Case Converter</h3>
<h3>Case Converter</h3>
<p>UPPER, lower, Title, camelCase, snake_case, kebab-case, PascalCase, mOcKiNg, and more.</p>
<p>UPPER, lower, Title, camelCase, snake_case, kebab-case, PascalCase, mOcKiNg, and more.</p>
<span class="card-domain">case.openvibe.tools</span>
<span class="card-domain">case.openvibe.tools</span>
@@ -87,13 +86,13 @@
@@ -87,13 +86,13 @@
<span class="card-domain">bubble.openvibe.tools</span>
<span class="card-domain">bubble.openvibe.tools</span>
</a>
</a>
<a href="https://count.openvibe.tools" class="tool-card">
<a href="https://count.openvibe.tools" class="tool-card">
- <div class="card-icon"><i class="fas fa-calculator"></i></div>
+ <div class="card-icon"><i class="fas fa-table-cells"></i></div>
<h3>Text Counter</h3>
<h3>Text Counter</h3>
<p>Count characters, words, sentences, paragraphs, bytes. Reading time and speaking time estimates.</p>
<p>Count characters, words, sentences, paragraphs, bytes. Reading time and speaking time estimates.</p>
<span class="card-domain">count.openvibe.tools</span>
<span class="card-domain">count.openvibe.tools</span>
</a>
</a>
<a href="https://clean.openvibe.tools" class="tool-card">
<a href="https://clean.openvibe.tools" class="tool-card">
- <div class="card-icon"><i class="fas fa-broom"></i></div>
+ <div class="card-icon"><i class="fas fa-eraser"></i></div>
<h3>Text Cleaner</h3>
<h3>Text Cleaner</h3>
<p>Fix smart quotes, remove invisible characters, strip Zalgo, normalize spacing, clean HTML tags.</p>
<p>Fix smart quotes, remove invisible characters, strip Zalgo, normalize spacing, clean HTML tags.</p>
<span class="card-domain">clean.openvibe.tools</span>
<span class="card-domain">clean.openvibe.tools</span>
@@ -105,7 +104,7 @@
@@ -105,7 +104,7 @@
<span class="card-domain">reversetext.openvibe.tools</span>
<span class="card-domain">reversetext.openvibe.tools</span>
</a>
</a>
<a href="https://kaomoji.openvibe.tools" class="tool-card">
<a href="https://kaomoji.openvibe.tools" class="tool-card">
- <div class="card-icon"><i class="fas fa-face-smile-beam"></i></div>
+ <div class="card-icon"><i class="fas fa-face-smile"></i></div>
<h3>Kaomoji & Emoticons</h3>
<h3>Kaomoji & Emoticons</h3>
<p>Browse and copy Japanese text emoticons sorted by mood: happy, sad, angry, love, shrug, lenny, and more.</p>
<p>Browse and copy Japanese text emoticons sorted by mood: happy, sad, angry, love, shrug, lenny, and more.</p>
<span class="card-domain">kaomoji.openvibe.tools</span>
<span class="card-domain">kaomoji.openvibe.tools</span>
@@ -141,13 +140,13 @@
@@ -141,13 +140,13 @@
<span class="card-domain">binary.openvibe.tools</span>
<span class="card-domain">binary.openvibe.tools</span>
</a>
</a>
<a href="https://markdown.openvibe.tools" class="tool-card">
<a href="https://markdown.openvibe.tools" class="tool-card">
- <div class="card-icon"><i class="fab fa-markdown"></i></div>
+ <div class="card-icon"><i class="fas fa-code"></i></div>
<h3>Markdown Preview</h3>
<h3>Markdown Preview</h3>
<p>Write Markdown on the left, see rendered HTML on the right. Export as HTML.</p>
<p>Write Markdown on the left, see rendered HTML on the right. Export as HTML.</p>
<span class="card-domain">markdown.openvibe.tools</span>
<span class="card-domain">markdown.openvibe.tools</span>
</a>
</a>
<a href="https://sort.openvibe.tools" class="tool-card">
<a href="https://sort.openvibe.tools" class="tool-card">
- <div class="card-icon"><i class="fas fa-sort-alpha-down"></i></div>
+ <div class="card-icon"><i class="fas fa-arrow-down-short-wide"></i></div>
<h3>Sort & Dedupe Lines</h3>
<h3>Sort & Dedupe Lines</h3>
<p>Sort lines alphabetically, numerically, or randomly. Remove duplicates instantly.</p>
<p>Sort lines alphabetically, numerically, or randomly. Remove duplicates instantly.</p>
<span class="card-domain">sort.openvibe.tools</span>
<span class="card-domain">sort.openvibe.tools</span>
@@ -191,7 +190,7 @@
@@ -191,7 +190,7 @@
<span class="card-domain">wordmark.openvibe.tools</span>
<span class="card-domain">wordmark.openvibe.tools</span>
</a>
</a>
<a href="https://badge.openvibe.tools" class="tool-card">
<a href="https://badge.openvibe.tools" class="tool-card">
- <div class="card-icon"><i class="fas fa-certificate"></i></div>
+ <div class="card-icon"><i class="fas fa-ranking-star"></i></div>
<h3>Badge & Sticker Maker</h3>
<h3>Badge & Sticker Maker</h3>
<p>Create badges, stickers, and labels with rounded shapes, bold text, and vibrant colors.</p>
<p>Create badges, stickers, and labels with rounded shapes, bold text, and vibrant colors.</p>
<span class="card-domain">badge.openvibe.tools</span>
<span class="card-domain">badge.openvibe.tools</span>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
@@ -65,7 +64,7 @@
@@ -65,7 +64,7 @@
<button class="btn btn-primary" onclick="beautify()"><i class="fas fa-wand-magic-sparkles"></i> Beautify</button>
<button class="btn btn-primary" onclick="beautify()"><i class="fas fa-wand-magic-sparkles"></i> Beautify</button>
<button class="btn btn-secondary" onclick="minify()"><i class="fas fa-compress"></i> Minify</button>
<button class="btn btn-secondary" onclick="minify()"><i class="fas fa-compress"></i> Minify</button>
<button class="btn btn-secondary" onclick="OpenVibeUI.copyText(lastFormatted)"><i class="fas fa-copy"></i> Copy Output</button>
<button class="btn btn-secondary" onclick="OpenVibeUI.copyText(lastFormatted)"><i class="fas fa-copy"></i> Copy Output</button>
- <button class="btn btn-secondary" onclick="sortKeys()"><i class="fas fa-arrow-down-a-z"></i> Sort Keys</button>
+ <button class="btn btn-secondary" onclick="sortKeys()"><i class="fas fa-arrow-down-wide-short"></i> Sort Keys</button>
</div>
</div>
</div>
</div>
@@ -18,14 +18,13 @@
@@ -18,14 +18,13 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
<body>
<body>
<div class="tool-wrap" style="max-width: 1000px;">
<div class="tool-wrap" style="max-width: 1000px;">
<div class="page-header">
<div class="page-header">
- <h1><i class="fas fa-face-smile-beam"></i> Kaomoji</h1>
+ <h1><i class="fas fa-face-smile"></i> Kaomoji</h1>
<p class="subtitle">Japanese text emoticons sorted by mood. Click any to copy. (◕‿◕)</p>
<p class="subtitle">Japanese text emoticons sorted by mood. Click any to copy. (◕‿◕)</p>
</div>
</div>
@@ -44,11 +43,11 @@
@@ -44,11 +43,11 @@
OpenVibeUI.initPage();
OpenVibeUI.initPage();
const container = document.getElementById('categories');
const container = document.getElementById('categories');
const moodIcons = {
const moodIcons = {
- happy:'fa-face-smile-beam', sad:'fa-face-sad-tear', angry:'fa-face-angry',
- love:'fa-heart', shrug:'fa-person-shrugging', surprise:'fa-face-surprise',
- cool:'fa-sunglasses', silly:'fa-face-grin-tongue', animals:'fa-paw',
+ happy:'fa-face-smile', sad:'fa-face-grin-squint-tears', angry:'fa-face-grin-squint',
+ love:'fa-heart', shrug:'fa-question', surprise:'fa-face-grin-stars',
+ cool:'fa-eye', silly:'fa-face-laugh', animals:'fa-paw',
music:'fa-music', tableFlip:'fa-table', tableUnflip:'fa-table',
music:'fa-music', tableFlip:'fa-table', tableUnflip:'fa-table',
- lenny:'fa-face-grin-wink', fight:'fa-hand-fist', sparkle:'fa-sparkles',
+ lenny:'fa-face-grin', fight:'fa-hand-fist', sparkle:'fa-sparkles',
};
};
for (const [mood, items] of Object.entries(TextOpenVibeEngine.kaomoji)) {
for (const [mood, items] of Object.entries(TextOpenVibeEngine.kaomoji)) {
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
@@ -53,7 +52,7 @@
@@ -53,7 +52,7 @@
<!-- Tools grid -->
<!-- Tools grid -->
<div class="section-header">
<div class="section-header">
- <i class="fas fa-tools"></i>
+ <i class="fas fa-screwdriver-wrench"></i>
<h2>Design Tools</h2>
<h2>Design Tools</h2>
</div>
</div>
<div class="tool-grid">
<div class="tool-grid">
@@ -70,7 +69,7 @@
@@ -70,7 +69,7 @@
<span class="card-domain">wordmark.openvibe.tools</span>
<span class="card-domain">wordmark.openvibe.tools</span>
</a>
</a>
<a href="https://badge.openvibe.tools" class="tool-card">
<a href="https://badge.openvibe.tools" class="tool-card">
- <div class="card-icon"><i class="fas fa-certificate"></i></div>
+ <div class="card-icon"><i class="fas fa-ranking-star"></i></div>
<h3>Badge & Sticker Maker</h3>
<h3>Badge & Sticker Maker</h3>
<p>Create rounded badges, stickers, and labels. Choose background shapes, colors, and text styles.</p>
<p>Create rounded badges, stickers, and labels. Choose background shapes, colors, and text styles.</p>
<span class="card-domain">badge.openvibe.tools</span>
<span class="card-domain">badge.openvibe.tools</span>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
@@ -49,7 +48,7 @@
@@ -49,7 +48,7 @@
<body>
<body>
<div class="tool-wrap" style="max-width: 1100px;">
<div class="tool-wrap" style="max-width: 1100px;">
<div class="page-header">
<div class="page-header">
- <h1><i class="fab fa-markdown"></i> Markdown Preview</h1>
+ <h1><i class="fas fa-code"></i> Markdown Preview</h1>
<p class="subtitle">Write Markdown on the left, see it rendered on the right. Export as HTML.</p>
<p class="subtitle">Write Markdown on the left, see it rendered on the right. Export as HTML.</p>
</div>
</div>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
@@ -136,7 +135,7 @@
@@ -136,7 +135,7 @@
</div>
</div>
<div class="swap-row">
<div class="swap-row">
- <button class="swap-btn" onclick="swapCurrencies()" title="Swap"><i class="fas fa-arrow-down-up-across-line"></i></button>
+ <button class="swap-btn" onclick="swapCurrencies()" title="Swap"><i class="fas fa-arrows-up-down-left-right"></i></button>
</div>
</div>
<div class="currency-box" id="box-bottom">
<div class="currency-box" id="box-bottom">
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
@@ -115,7 +114,7 @@
@@ -115,7 +114,7 @@
<body>
<body>
<div class="tool-wrap">
<div class="tool-wrap">
<div class="page-header">
<div class="page-header">
- <h1><i class="fas fa-pepper-hot"></i> Mexican Slang</h1>
+ <h1><i class="fas fa-fire"></i> Mexican Slang</h1>
<p class="subtitle">Real Mexican Spanish — the words they don't teach you in class. ¡No mames, güey!</p>
<p class="subtitle">Real Mexican Spanish — the words they don't teach you in class. ¡No mames, güey!</p>
</div>
</div>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
@@ -18,14 +18,13 @@
@@ -18,14 +18,13 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
<body>
<body>
<div class="tool-wrap">
<div class="tool-wrap">
<div class="page-header">
<div class="page-header">
- <h1><i class="fas fa-sort-alpha-down"></i> Sort & Dedupe Lines</h1>
+ <h1><i class="fas fa-arrow-down-short-wide"></i> Sort & Dedupe Lines</h1>
<p class="subtitle">Sort lines alphabetically or reverse. Remove duplicate lines instantly.</p>
<p class="subtitle">Sort lines alphabetically or reverse. Remove duplicate lines instantly.</p>
</div>
</div>
@@ -41,11 +40,11 @@ banana
@@ -41,11 +40,11 @@ banana
elderberry</textarea>
elderberry</textarea>
</div>
</div>
<div class="btn-row">
<div class="btn-row">
- <button class="btn btn-primary" onclick="sortAsc()"><i class="fas fa-sort-alpha-down"></i> Sort A→Z</button>
- <button class="btn btn-secondary" onclick="sortDesc()"><i class="fas fa-sort-alpha-up"></i> Sort Z→A</button>
- <button class="btn btn-secondary" onclick="shuffle()"><i class="fas fa-shuffle"></i> Shuffle</button>
+ <button class="btn btn-primary" onclick="sortAsc()"><i class="fas fa-arrow-down-short-wide"></i> Sort A→Z</button>
+ <button class="btn btn-secondary" onclick="sortDesc()"><i class="fas fa-arrow-down-wide-short"></i> Sort Z→A</button>
+ <button class="btn btn-secondary" onclick="shuffle()"><i class="fas fa-right-left"></i> Shuffle</button>
<button class="btn btn-secondary" onclick="dedupe()"><i class="fas fa-filter"></i> Remove Duplicates</button>
<button class="btn btn-secondary" onclick="dedupe()"><i class="fas fa-filter"></i> Remove Duplicates</button>
- <button class="btn btn-ghost" onclick="reverseLines()"><i class="fas fa-retweet"></i> Reverse Order</button>
+ <button class="btn btn-ghost" onclick="reverseLines()"><i class="fas fa-arrow-right-arrow-left"></i> Reverse Order</button>
</div>
</div>
</div>
</div>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
@@ -56,11 +55,11 @@
@@ -56,11 +55,11 @@
const collected = document.getElementById('collected');
const collected = document.getElementById('collected');
const categoryIcons = {
const categoryIcons = {
- arrows: 'fa-right-long', stars: 'fa-star', hearts: 'fa-heart', music: 'fa-music',
- math: 'fa-superscript', currency: 'fa-dollar-sign', bullets: 'fa-list',
- boxes: 'fa-square', lines: 'fa-grip-lines', dividers: 'fa-minus',
- check: 'fa-check', zodiac: 'fa-star-of-david', weather: 'fa-cloud-sun',
- gaming: 'fa-chess', hands: 'fa-hand', faces: 'fa-face-smile',
+ arrows: 'fa-arrow-right', stars: 'fa-star', hearts: 'fa-heart', music: 'fa-music',
+ math: 'fa-arrow-up', currency: 'fa-dollar-sign', bullets: 'fa-list',
+ boxes: 'fa-square', lines: 'fa-grip-vertical', dividers: 'fa-minus',
+ check: 'fa-check', zodiac: 'fa-star', weather: 'fa-cloud-sun',
+ gaming: 'fa-gamepad', hands: 'fa-hand', faces: 'fa-face-smile',
};
};
for (const [name, syms] of Object.entries(TextOpenVibeEngine.symbols)) {
for (const [name, syms] of Object.entries(TextOpenVibeEngine.symbols)) {
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
@@ -73,7 +72,7 @@
@@ -73,7 +72,7 @@
<h3><i class="fas fa-image"></i> Background</h3>
<h3><i class="fas fa-image"></i> Background</h3>
<label class="upload-btn" id="uploadBtn">
<label class="upload-btn" id="uploadBtn">
<input type="file" id="fileInput" accept="image/*">
<input type="file" id="fileInput" accept="image/*">
- <i class="fas fa-cloud-upload-alt"></i> Upload Background Image
+ <i class="fas fa-cloud-arrow-up"></i> Upload Background Image
</label>
</label>
<div class="ctrl-row" style="margin-top: .75rem;">
<div class="ctrl-row" style="margin-top: .75rem;">
<label>BG Color</label>
<label>BG Color</label>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
@@ -83,7 +82,7 @@
@@ -83,7 +82,7 @@
</div>
</div>
<div class="control-group">
<div class="control-group">
- <h3><i class="fas fa-fill-drip"></i> Colors</h3>
+ <h3><i class="fas fa-droplet"></i> Colors</h3>
<div class="control-row">
<div class="control-row">
<label>Text Color</label>
<label>Text Color</label>
<input type="color" id="color" value="#ffffff">
<input type="color" id="color" value="#ffffff">
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
@@ -56,7 +55,7 @@
@@ -56,7 +55,7 @@
<div>
<div>
<div class="drop-zone" id="dropZone" onclick="document.getElementById('fileInput').click()">
<div class="drop-zone" id="dropZone" onclick="document.getElementById('fileInput').click()">
<input type="file" id="fileInput" accept="image/*">
<input type="file" id="fileInput" accept="image/*">
- <i class="fas fa-cloud-upload-alt" style="font-size: 2rem; color: var(--text-muted); margin-bottom: .5rem; display: block;"></i>
+ <i class="fas fa-cloud-arrow-up" style="font-size: 2rem; color: var(--text-muted); margin-bottom: .5rem; display: block;"></i>
<p style="color: var(--text-secondary); margin: 0;">Click or drag an image here</p>
<p style="color: var(--text-secondary); margin: 0;">Click or drag an image here</p>
</div>
</div>
<div class="wm-preview" id="previewArea" style="margin-top: 1.5rem;">
<div class="wm-preview" id="previewArea" style="margin-top: 1.5rem;">
@@ -80,7 +79,7 @@
@@ -80,7 +79,7 @@
</div>
</div>
<div class="ctrl-group">
<div class="ctrl-group">
- <h3><i class="fas fa-crosshairs"></i> Position</h3>
+ <h3><i class="fas fa-location-dot"></i> Position</h3>
<div class="pos-grid" id="posGrid">
<div class="pos-grid" id="posGrid">
<button class="pos-btn" data-pos="top-left">↖ Top Left</button>
<button class="pos-btn" data-pos="top-left">↖ Top Left</button>
<button class="pos-btn" data-pos="top-center">↑ Top</button>
<button class="pos-btn" data-pos="top-center">↑ Top</button>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<style>
<style>
@@ -18,7 +18,6 @@
@@ -18,7 +18,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/shared.css">
<link rel="stylesheet" href="/css/shared.css">
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
</head>
</head>
@@ -60,12 +59,12 @@
@@ -60,12 +59,12 @@
<div class="btn-row">
<div class="btn-row">
<button class="btn btn-primary" onclick="copyOutput()"><i class="fas fa-copy"></i> Copy</button>
<button class="btn btn-primary" onclick="copyOutput()"><i class="fas fa-copy"></i> Copy</button>
<button class="btn btn-secondary" onclick="regenerate()"><i class="fas fa-rotate"></i> Regenerate</button>
<button class="btn btn-secondary" onclick="regenerate()"><i class="fas fa-rotate"></i> Regenerate</button>
- <button class="btn btn-ghost" onclick="OpenVibeUI.shareText(lastOutput)"><i class="fas fa-share"></i> Share</button>
+ <button class="btn btn-ghost" onclick="OpenVibeUI.shareText(lastOutput)"><i class="fas fa-arrow-up-from-bracket"></i> Share</button>
</div>
</div>
</div>
</div>
<div class="tool-section">
<div class="tool-section">
- <h2><i class="fas fa-broom"></i> Need to clean Zalgo?</h2>
+ <h2><i class="fas fa-eraser"></i> Need to clean Zalgo?</h2>
<p style="color: var(--text-secondary); margin-bottom: 12px;">Paste Zalgo text and strip all the combining characters.</p>
<p style="color: var(--text-secondary); margin-bottom: 12px;">Paste Zalgo text and strip all the combining characters.</p>
<div class="input-group">
<div class="input-group">
<textarea id="cleanInput" placeholder="Paste Zalgo text to clean..." rows="3"></textarea>
<textarea id="cleanInput" placeholder="Paste Zalgo text to clean..." rows="3"></textarea>
@@ -64,9 +64,9 @@ app.use(helmet({
@@ -64,9 +64,9 @@ app.use(helmet({
contentSecurityPolicy: {
contentSecurityPolicy: {
directives: {
directives: {
defaultSrc: ["'self'"],
defaultSrc: ["'self'"],
- scriptSrc: ["'self'", "'unsafe-inline'", "cdnjs.cloudflare.com", "cdn.jsdelivr.net", "fonts.googleapis.com", "https://openvibe.network"],
- styleSrc: ["'self'", "'unsafe-inline'", "cdnjs.cloudflare.com", "fonts.googleapis.com", "fonts.gstatic.com"],
- fontSrc: ["'self'", "fonts.gstatic.com", "cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "cdn.jsdelivr.net", "fonts.googleapis.com", "https://openvibe.network"],
+ styleSrc: ["'self'", "'unsafe-inline'", "fonts.googleapis.com", "fonts.gstatic.com"],
+ fontSrc: ["'self'", "fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "blob:"],
imgSrc: ["'self'", "data:", "blob:"],
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
connectSrc: ["'self'", "https://openvibe.network", "https://api.exchangerate-api.com", "https://openvibe.events"],
connectSrc: ["'self'", "https://openvibe.network", "https://api.exchangerate-api.com", "https://openvibe.events"],
@@ -19,7 +19,7 @@
@@ -19,7 +19,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"uuid": "^11.1.0"
"uuid": "^11.1.0"
}
}
@@ -1005,9 +1005,9 @@
@@ -1005,9 +1005,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.56.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.56.0",
- "integrity": "sha512-6Whnuat9QQmKLt7ncQ+xtmwAAZfRUUIouLb/cFlSm6SvL5DfXkpsb44nscAqkx2vUIpDFLtT0nnD37jNCP+Cxg==",
+ "version": "4.73.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.73.0",
+ "integrity": "sha512-F8+EPs8QCabUXyyl/s/cS5JPhLTDZ4DRR7pkfMCNk3ManuqasijjNU2X6zE7YxaI8C+ltdnQEafHbgX98XYGcA==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -17,7 +17,6 @@
@@ -17,7 +17,6 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/style.css">
</head>
</head>
<body>
<body>
@@ -90,7 +89,7 @@
@@ -90,7 +89,7 @@
<i class="fa-solid fa-download"></i> Save File
<i class="fa-solid fa-download"></i> Save File
</a>
</a>
<button class="btn btn-secondary" id="new-btn">
<button class="btn btn-secondary" id="new-btn">
- <i class="fa-solid fa-arrow-rotate-left"></i> Download Another
+ <i class="fa-solid fa-rotate-left"></i> Download Another
</button>
</button>
</div>
</div>
</section>
</section>
@@ -99,7 +98,7 @@
@@ -99,7 +98,7 @@
<section class="error-section" id="error-section" style="display:none">
<section class="error-section" id="error-section" style="display:none">
<div class="error-icon"><i class="fa-solid fa-circle-exclamation"></i></div>
<div class="error-icon"><i class="fa-solid fa-circle-exclamation"></i></div>
<p id="error-msg"></p>
<p id="error-msg"></p>
- <button class="btn btn-secondary" id="retry-btn"><i class="fa-solid fa-arrow-rotate-left"></i> Try Again</button>
+ <button class="btn btn-secondary" id="retry-btn"><i class="fa-solid fa-rotate-left"></i> Try Again</button>
</section>
</section>
</div>
</div>
@@ -79,9 +79,9 @@ app.use(helmet({
@@ -79,9 +79,9 @@ app.use(helmet({
contentSecurityPolicy: {
contentSecurityPolicy: {
directives: {
directives: {
defaultSrc: ["'self'"],
defaultSrc: ["'self'"],
- scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://openvibe.network", "https://static.cloudflareinsights.com"],
- styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
- fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
+ scriptSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://openvibe.network", "https://static.cloudflareinsights.com"],
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
+ fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "https://i.ytimg.com", "https://img.youtube.com", "https://*.ggpht.com", "https://openvibe.network", "https://openvibe.media"],
imgSrc: ["'self'", "data:", "https://i.ytimg.com", "https://img.youtube.com", "https://*.ggpht.com", "https://openvibe.network", "https://openvibe.media"],
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
// openvibe.events: release notifications (release-watch's EventSource, openvibe-shared 2.13.0).
connectSrc: ["'self'", "https://openvibe.network", "https://openvibe.tools", "https://*.openvibe.tools", "https://cloudflareinsights.com", "https://openvibe.events"],
connectSrc: ["'self'", "https://openvibe.network", "https://openvibe.tools", "https://*.openvibe.tools", "https://cloudflareinsights.com", "https://openvibe.events"],
new file mode 100644
new file mode 100644
@@ -0,0 +1,83 @@
@@ -0,0 +1,83 @@
+'use strict';
+// No Font Awesome (plan L54 K4; the owner, 2026-11-01: "get rid of Font Awesome completely and just use our own
+// openvibe.design system"). OpenVibe.Tools's icons are the OpenVibe glyphs (openvibe-shared/glyphs, drawn in the browser by
+// /shared/ov-glyphs.js, which the Shared navbar loads on a page that names an icon) and the brand logos from
+// openvibe-shared/brands. This holds it there:
+// - no page or stylesheet links Font Awesome, and nothing draws with Font Awesome's font;
+// - every page that names an icon loads the navbar or the glyph drop-in;
+// - every fa- name OpenVibe.Tools uses is one the glyphs or the brands draw (a name nobody draws would show nothing).
+// node test/no-font-awesome.test.js
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const ROOT = path.join(__dirname, '..');
+// openvibe-shared is pinned per app here (each app has its own node_modules), so the glyphs come from one of them.
+const sharedDir = fs.existsSync(path.join(ROOT, 'node_modules', 'openvibe-shared'))
+ ? path.join(ROOT, 'node_modules', 'openvibe-shared')
+ : path.join(ROOT, 'apps', 'gateway', 'node_modules', 'openvibe-shared');
+const glyphs = require(path.join(sharedDir, 'glyphs'));
+const brands = require(path.join(sharedDir, 'brands'));
+
+const walk = (dir, ok, out = []) => {
+ if (!fs.existsSync(dir)) return out;
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
+ if (e.name === 'node_modules' || e.name === 'vendor' || e.name.startsWith('.')) continue;
+ const p = path.join(dir, e.name);
+ if (e.isDirectory()) walk(p, ok, out); else if (ok.test(e.name)) out.push(p);
+ }
+ return out;
+};
+const rel = (p) => path.relative(ROOT, p);
+
+// The pages (whole HTML files) and the code that draws them.
+const PAGE_ROOTS = ["apps"];
+const CODE_ROOTS = ["apps"];
+const LAYOUTS = []; // the server-rendered pages: their layout brings the navbar or the glyphs
+
+// A Font Awesome stylesheet, wherever it is written.
+const FA_CSS = /fontawesome[^"'\s]*\.css|font-awesome[^"'\s]*\.css|cdnjs\.cloudflare\.com\/ajax\/libs\/font-awesome/i;
+
+const pages = [];
+for (const r of PAGE_ROOTS) pages.push(...walk(path.join(ROOT, r), /\.html$/));
+for (const p of pages) {
+ const html = fs.readFileSync(p, 'utf8');
+ assert.ok(!/fontawesome|font-awesome/i.test(html), `${rel(p)} links Font Awesome`);
+ // A whole page (with a <head>; fragments are parts of a page) that names an icon loads what draws it.
+ if (/<head[\s>]/i.test(html) && /\bfa-[a-z]/.test(html) && !/\/shared\/navbar|ov-glyphs\.js/.test(html)) {
+ assert.fail(`${rel(p)} has fa- icons but loads neither the navbar nor /shared/ov-glyphs.js`);
+ }
+}
+
+// Nothing draws with Font Awesome's font.
+for (const r of CODE_ROOTS) {
+ for (const p of walk(path.join(ROOT, r), /\.css$/)) {
+ assert.ok(!/font-family:\s*["']?Font ?Awesome/i.test(fs.readFileSync(p, 'utf8')), `${rel(p)} draws with Font Awesome's font`);
+ }
+ for (const p of walk(path.join(ROOT, r), /\.(html|js|mjs|cjs)$/)) {
+ const t = fs.readFileSync(p, 'utf8');
+ assert.ok(!/['"]\d+px FontAwesome|Font Awesome 6 Free/.test(t), `${rel(p)} draws with Font Awesome's font`);
+ assert.ok(!FA_CSS.test(t), `${rel(p)} links Font Awesome`);
+ }
+}
+
+// The server-rendered pages get the glyphs from the Shared navbar (shell.page) or from the drop-in.
+for (const f of LAYOUTS) {
+ const t = fs.readFileSync(path.join(ROOT, f), 'utf8');
+ assert.ok(/shell\.page|ov-glyphs\.js|\/shared\/navbar/.test(t), `${f} renders pages without the navbar or /shared/ov-glyphs.js`);
+}
+
+// Every fa- name in the pages, the scripts and the server views is drawn by a glyph or a brand.
+const MOD = /^(solid|regular|light|thin|duotone|brands|sharp|classic|fw|lg|xl|xs|sm|2xs|2xl|\d+x|li|ul|border|inverse|pull-left|pull-right|rotate-\d+|rotate-by|flip-horizontal|flip-vertical|flip-both|width-auto|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|stack|stack-1x|stack-2x|layers|layers-text|layers-counter|swap-opacity|stack-text)$/;
+const files = [];
+for (const r of CODE_ROOTS) files.push(...walk(path.join(ROOT, r), /\.(html|js)$/));
+const missing = new Map();
+for (const f of files) {
+ for (const m of fs.readFileSync(f, 'utf8').matchAll(/\bfa-([a-z0-9]+(?:-[a-z0-9]+)*)\b/g)) {
+ const n = m[1];
+ if (MOD.test(n) || glyphs.fromFa(n) || brands.fromFa(n)) continue;
+ if (!missing.has(n)) missing.set(n, rel(f));
+ }
+}
+assert.deepStrictEqual([...missing].map(([n, f]) => `fa-${n} (${f})`), [], 'every icon this site names is drawn by the OpenVibe glyphs or brands');
+
+console.log(`no font awesome: ${pages.length} pages, ${files.length} files, every fa- name drawn`);