1<!doctype html> 2<html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <link rel="icon" href="/_genesis/assets/favicon" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 <title>Workspace Portal â Built with Taskade</title> 8 9
9<script> 10 // Pre-React error catch-all. Handles two scenarios: 11 // 1. React never mounts (syntax error, import crash) â spinner is still in #root 12 // 2. React mounted, crashed without ErrorBoundary, and unmounted â #root is empty 13 (function() { 14 var esc = function(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }; 15 var errorHtml = function(msg) { 16 return '<div style="position:fixed;inset:0;background:#000;padding:2rem;font-family:system-ui,sans-serif;color:#fff;overflow:auto">' + 17 '<div style="max-width:600px;margin:0 auto">' + 18 '<h2 style="margin:0 0 0.5rem;font-size:1.25rem">Something went wrong</h2>' + 19 '<p style="color:#999;margin:0 0 1rem;font-size:0.95rem">The app failed to load. Try refreshing the page.</p>' + 20 '<pre style="background:#1a1a1a;padding:1rem;border-radius:8px;overflow:auto;font-size:0.8rem;color:#f87171;margin:0;white-space:pre-wrap;word-break:break-word">' + 21 esc(msg) + '</pre></div></div>'; 22 }; 23 var reported = false; 24 function reportError(code, msg, stack) { 25 if (reported) return; 26 reported = true; 27 try { 28 var logger = window.__TASKADE_APP_LIFECYCLE_LOGGER__; 29 if (typeof logger?.log === 'function') { logger.log({ level: 'error', message: 'Runtime Error', data: { code: code, message: msg, stack: stack || '' } }); } 30 } catch (e) { /* Swallow â never block the fallback error UI */ } 31 } 32 function showError(code, msg, stack) { 33 var root = document.getElementById('root'); 34 if (!root) return; 35 var spinner = root.querySelector('.genesis__spinner-container'); 36 if (spinner) { 37 // Scenario 1: React never mounted â replace spinner 38 reportError('error.fatal', msg, stack); 39 spinner.outerHTML = errorHtml(msg); 40 } else if (!root.childElementCount) { 41 // Scenario 2: React crashed and unmounted â #root is empty 42 reportError('error.fatal', msg, stack); 43 root.innerHTML = errorHtml(msg); 44 } else { 45 // React is running fine â don't interfere with the DOM, 46 // but still report with the original code for diagnostics 47 reportError(code, msg, stack); 48 } 49 } 50 window.addEventListener('error', function(e) { 51 var msg = e.message || String(e.error) || 'Unknown error'; 52 var stack = e.error && e.error.stack ? e.error.stack : (e.filename ? e.filename + ':' + e.lineno + ':' + e.colno : ''); 53 showError('error.runtime_error', msg, stack); 54 }); 55 window.addEventListener('unhandledrejection', function(e) { 56 var msg = e.reason ? String(e.reason) : 'Unhandled promise rejection'; 57 var stack = e.reason && e.reason.stack ? e.reason.stack : ''; 58 showError('error.unhandled_promise', msg, stack); 59 }); 60 })(); 61 </script>
61 62 <style> 63 /* Genesis spinner styles */ 64 .genesis__spinner-container { 65 position: fixed; 66 inset: 0; 67 background: #000000; 68 display: flex; 69 align-items: center; 70 justify-content: center; 71 z-index: 9999; 72 } 73 74 .genesis__spinner-ring { 75 width: 20px; 76 height: 20px; 77 border: 1.5px solid transparent; 78 border-top: 1.5px solid rgba(255, 255, 255, 0.6); 79 border-radius: 50%; 80 animation: genesis__spin 1s linear infinite; 81 } 82 83 @keyframes genesis__spin { 84 0% { transform: rotate(0deg); } 85 100% { transform: rotate(360deg); } 86 } 87 88 /* Respect reduced motion preferences */ 89 @media (prefers-reduced-motion: reduce) { 90 .genesis__spinner-ring { 91 animation: none; 92 border-top-color: rgba(255, 255, 255, 0.9); 93 } 94 } 95 </style> 96 97 98
99<script type="module" src="/assets/main-LIU47OKU.js"></script>
vendor: 1 bytes, line 99
99
100<script type="module" src="/chunks/highlighted-body-OFNGDK62-76QFM5J6.js"></script>
vendor: 1 bytes, line 100
100
101<script type="module" src="/chunks/mermaid-GHXKKRXX-SUCXO7VR.js"></script>
vendor: 1 bytes, line 101
101
102<script type="module" src="/chunks/chunk-DSUKE27G.js"></script>
102 103 104 105<link rel="stylesheet" href="/assets/main-VSDFLXXY.css" /> 106 <meta charset="utf-8" /><meta name="language" content="en-US" /><meta name="robots" content="index" /><meta name="author" content="Taskade" /><link rel="icon" sizes="32x32" href="/_genesis/assets/favicon" /><link rel="apple-touch-icon" sizes="180x180" href="/_genesis/assets/favicon" /><meta name="title" content="Workspace Portal â Built with Taskade" /><meta property="og:title" content="Workspace Portal â Built with Taskade" /><meta name="twitter:title" content="Workspace Portal â Built with Taskade" /><meta name="description" content="A living app built with Taskade Genesis. One prompt, one app. Projects remember, agents learn, and workflows move." /><meta property="og:description" content="A living app built with Taskade Genesis. One prompt, one app. Projects remember, agents learn, and workflows move." /><meta name="twitter:description" content="A living app built with Taskade Genesis. One prompt, one app. Projects remember, agents learn, and workflows move." /><meta property="og:type" content="website" /><meta property="og:url" content="https://workspace-portal-7084.taskade.app" /><meta property="og:site_name" content="Taskade" /><meta property="og:image" content="https://workspace-portal-7084.taskade.app/_internal/screenshot" /><meta property="og:image:alt" content="Workspace Portal â Built with Taskade - A living app built with Taskade Genesis. One prompt, one app. Projects remember, agents learn, and w..." /><meta property="og:locale" content="en_US" /><meta name="twitter:card" content="summary_large_image" /><meta name="twitter:site" content="@taskade" /><meta name="twitter:creator" content="@taskade" /><meta name="twitter:image" content="https://workspace-portal-7084.taskade.app/_internal/screenshot" /><meta name="twitter:image:alt" content="Workspace Portal â Built with Taskade - A living app built with Taskade Genesis. One prompt, one app. Projects remember, agents learn, and w..." /><meta name="format-detection" content="telephone=no" /><meta name="mobile-web-app-capable" content="yes" /><meta name="apple-mobile-web-app-capable" content="yes" /><meta name="apple-mobile-web-app-status-bar-style" content="default" /><meta name="apple-mobile-web-app-title" content="Workspace Portal â Built with Taskade" /><meta name="application-name" content="Taskade" /><link rel="canonical" href="https://workspace-portal-7084.taskade.app" />
106<script type="application/ld+json"> 107 { 108 "@context": "https://schema.org", 109 "@type": "WebSite", 110 "name": "Taskade", 111 "alternateName": "Workspace Portal â Built with Taskade", 112 "description": "A living app built with Taskade Genesis. One prompt, one app. Projects remember, agents learn, and workflows move.", 113 "image": "https://workspace-portal-7084.taskade.app/_internal/screenshot", 114 "url": "https://workspace-portal-7084.taskade.app", 115 "author": { 116 "@type": "Organization", 117 "name": "Taskade" 118 }, 119 "publisher": { 120 "@type": "Organization", 121 "name": "Taskade", 122 "url": "https://workspace-portal-7084.taskade.app", 123 "logo": { 124 "@type": "ImageObject", 125 "url": "https://workspace-portal-7084.taskade.app/_internal/screenshot" 126 } 127 } 128 } 129</script>
129<script defer src="/_genesis/vendor/flock.min.js" data-proxy-url="/_genesis/api/events" data-tenant-id="yff1nldcuoxgz1n2:default" data-domain="workspace-portal-7084.taskade.app"></script>
129<script defer src="/_genesis/vendor/scroll-guard.js"></script>
129</head> 130 <body>
130<script> 131 (function() { 132 if (window.__genesisErrorCatchAll) return; 133 window.__genesisErrorCatchAll = true; 134 var esc = function(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }; 135 var errorHtml = function(msg, id) { 136 return '<div style="position:fixed;inset:0;background:#000;padding:2rem;font-family:system-ui,sans-serif;color:#fff;overflow:auto">' + 137 '<div style="max-width:600px;margin:0 auto">' + 138 '<h2 style="margin:0 0 0.5rem;font-size:1.25rem">Something went wrong</h2>' + 139 '<p style="color:#999;margin:0 0 1rem;font-size:0.95rem">The app failed to load. Try refreshing the page.</p>' + 140 '<pre style="background:#1a1a1a;padding:1rem;border-radius:8px;overflow:auto;font-size:0.8rem;color:#f87171;margin:0;white-space:pre-wrap;word-break:break-word">' + 141 esc(msg) + '<\/pre>' + 142 '<p style="color:#666;margin:1rem 0 0;font-size:0.75rem">Error ID: <code>' + esc(id) + '<\/code><\/p>' + 143 '<\/div><\/div>'; 144 }; 145 var reported = false; 146 var errorId = null; 147 function getErrorId() { 148 if (errorId != null) return errorId; 149 try { 150 if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { errorId = crypto.randomUUID().replace(/-/g, ''); return errorId; } 151 } catch (e) { /* Fall through to the time+random id below */ } 152 errorId = Date.now().toString(36) + Math.random().toString(36).slice(2, 12); 153 return errorId; 154 } 155 function reportError(code, msg, stack) { 156 if (reported) return; 157 reported = true; 158 try { 159 var logger = window.__TASKADE_APP_LIFECYCLE_LOGGER__; 160 if (typeof logger?.log === 'function') { 161 logger.log({ level: 'error', message: 'Runtime Error', data: { code: code, message: msg, stack: stack || '' } }); 162 return; 163 } 164 // Published: no logger, so beacon to the Taskade gateway instead of 165 // dropping the crash on the floor (#27626). Relative path â the 166 // Director proxy injects the gateway token server-side. Once per page 167 // session (the latch above) so a crash loop cannot flood the sink. 168 fetch('/api/taskade/telemetry/errors', { 169 method: 'POST', 170 headers: { 'Content-Type': 'application/json' }, 171 body: JSON.stringify({ 172 errorId: getErrorId(), 173 code: code, 174 message: String(msg || 'Unknown error').slice(0, 4000), 175 stack: String(stack || '').slice(0, 20000), 176 path: location.pathname.slice(0, 1000),
177 userAgent: navigator.userAgent.slice(0, 512) 178 }), 179 keepalive: true 180 // Default same-origin credentials: a password-protected app gates 181 // /api/taskade/* on its signed access cookie, so omitting them 182 // would redirect the beacon to the password page. 183 }).catch(function() {}); 184 } catch (e) { /* Swallow â never block the fallback error UI */ } 185 } 186 function showError(code, msg, stack) { 187 var root = document.getElementById('root'); 188 if (!root) return; 189 var spinner = root.querySelector('.genesis__spinner-container'); 190 if (spinner) { 191 // Scenario 1: React never mounted â replace spinner 192 reportError('error.fatal', msg, stack); 193 spinner.outerHTML = errorHtml(msg, getErrorId()); 194 } else if (!root.childElementCount) { 195 // Scenario 2: React crashed and unmounted â #root is empty 196 reportError('error.fatal', msg, stack); 197 root.innerHTML = errorHtml(msg, getErrorId()); 198 } else { 199 // React is running fine â don't interfere with the DOM, 200 // but still report with the original code for diagnostics 201 reportError(code, msg, stack); 202 } 203 } 204 window.addEventListener('error', function(e) { 205 var msg = e.message || String(e.error) || 'Unknown error'; 206 var stack = e.error && e.error.stack ? e.error.stack : (e.filename ? e.filename + ':' + e.lineno + ':' + e.colno : ''); 207 showError('error.runtime_error', msg, stack); 208 }); 209 window.addEventListener('unhandledrejection', function(e) { 210 var msg = e.reason ? String(e.reason) : 'Unhandled promise rejection'; 211 var stack = e.reason && e.reason.stack ? e.reason.stack : ''; 212 showError('error.unhandled_promise', msg, stack); 213 }); 214 // A dead <img> degrades to a neutral placeholder instead of painting its 215 // raw alt string across the page (#27730) â the symptom half of the 216 // fabricated-hot-link fix, and the only half that helps an ALREADY 217 // published app, whose images can rot long after generation. 218 // 219 // Resource load errors do not bubble, so they reach window only in the 220 // CAPTURE phase. That is why neither handler above has ever seen one, and 221 // why this cannot disturb them: a broken image must never render the fatal 222 // error overlay or spend the once-per-session crash beacon. 223 // 224 // Swapping in a 1x1 transparent GIF is what actually stops the alt text â 225 // the element becomes a SUCCESSFUL load, so the browser has no broken image 226 // to caption, while the alt attribute stays for assistive tech. Layout is 227 // untouched: the app's own width/height/aspect classes still apply. The grey 228 // is translucent so it reads correctly on a light or a dark surface without 229 // this script having to know the app's theme. 230 var deadImagePixel = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; 231 window.addEventListener('error', function(e) { 232 var el = e.target; 233 if (!el || el.tagName !== 'IMG') return; 234 // The swap fires a fresh load on the same element, so the latch is what 235 // keeps a failing data URI from looping. 236 if (el.getAttribute('data-genesis-image-fallback') !== null) return; 237 el.setAttribute('data-genesis-image-fallback', ''); 238 el.removeAttribute('srcset'); 239 el.src = deadImagePixel; 240 el.style.background = 'linear-gradient(135deg, rgba(127,127,127,0.16), rgba(127,127,127,0.28))'; 241 }, true); 242 243 // --------------------------------------------------------------- 244 // Dead env reads (advisory, NOT a failure) 245 // --------------------------------------------------------------- 246 // The bundle's import.meta.env / process.env shims are reporting 247 // Proxies (see genesisEnvBanner.ts). They are value-preserving: a 248 // missing key still reads undefined. This turns that read from invisible 249 // into observable, because a feature gated on one is permanently off and 250 // has no other symptom â the app builds, boots and looks healthy. 251 // 252 // Deliberately WARN, never error. The runtime cannot see whether a call 253 // site has a working fallback, so a correct 254 // VITE_API_BASE_URL || 'https://real' is reported too. Warn keeps it
255 // out of the "Fix with AI" popup set and out of FATAL_LIFECYCLE_CODES, 256 // both of which are error-level allowlists. 257 var deadEnvSeen = {}; 258 var deadEnvSent = 0; 259 // Its OWN budget and latch: sharing the 'reported' latch above would let 260 // an advisory notice spend the one crash report this page session gets. 261 var DEAD_ENV_MAX = 5; 262 function reportDeadEnv(source, key) { 263 try { 264 var signature = source + '.' + key; 265 if (deadEnvSeen[signature] || deadEnvSent >= DEAD_ENV_MAX) return; 266 deadEnvSeen[signature] = 1; 267 deadEnvSent++; 268 var msg = signature + ' is read but a Genesis build never defines it, so the value is ' + 269 'always undefined. If it gates a feature, that feature is permanently off. Route ' + 270 'secret-bearing calls through GenesisClient.proxy() with a secretAlias instead.'; 271 var logger = window.__TASKADE_APP_LIFECYCLE_LOGGER__; 272 if (typeof logger?.log === 'function') { 273 logger.log({ level: 'warn', message: 'Dead env read', data: { code: 'warn.dead_env', message: msg } }); 274 return; 275 } 276 fetch('/api/taskade/telemetry/errors', { 277 method: 'POST', 278 headers: { 279 'Content-Type': 'application/json', 280 // Lets the publish Worker pick the advisory rate-limit bucket 281 // without reading this body. Doing that at the edge means I/O 282 // of unbounded duration ahead of the limiter meant to bound it, 283 // which is the whole class of problem the header avoids. The 284 // origin still classifies from the validated body, so this is a 285 // bucket hint and never the record (#29164). 286 'X-Genesis-Telemetry-Code': 'warn.dead_env' 287 }, 288 body: JSON.stringify({ 289 errorId: 'de' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8), 290 code: 'warn.dead_env', 291 message: msg.slice(0, 4000), 292 path: location.pathname.slice(0, 1000), 293 // The source and key as STRUCTURE, not only inside the prose. 294 // The gateway fingerprints on app + code alone, because this 295 // endpoint's token ships in the bundle and free text from it 296 // cannot be trusted; these two validated fields are how the 297 // key still reaches Sentry, as a tag. 298 // Sliced to the schema's bound like every other field here: the 299 // banner filters the key's SHAPE but not its length, and an 300 // over-long key would fail validation and lose the whole 301 // report rather than just the tag. 302 envSource: source, 303 envKey: String(key).slice(0, 64) 304 }), 305 keepalive: true 306 }).catch(function() {}); 307 } catch (e) { /* Swallow â advisory telemetry must never break an app */ } 308 } 309 window.__genesisReportDeadEnv = reportDeadEnv; 310 // Drain anything the bundle's banner queued. This script is a classic 311 // inline script and the bundle is a deferred module, so in practice the 312 // reporter wins the race; the queue is what makes that not a matter of 313 // luck (and covers an app whose HTML predates this script). 314 try { 315 var queued = window.__genesisDeadEnvQueue; 316 if (queued) { 317 window.__genesisDeadEnvQueue = []; 318 for (var qi = 0; qi < queued.length; qi++) reportDeadEnv(queued[qi][0], queued[qi][1]); 319 } 320 } catch (e) { /* Ignore. */ } 321 })(); 322</script>
322 323 <div id="root"> 324 <div class="genesis__spinner-container"> 325 <div class="genesis__spinner-ring"></div> 326 </div> 327 </div> 328
328<script> 329 (function() { 330 if (typeof window === 'undefined' || typeof document === 'undefined') { 331 return; 332 } 333 if (typeof window.gtag === 'function') { 334 return; 335 } 336 window.dataLayer = window.dataLayer || []; 337 function gtag(){window.dataLayer.push(arguments);} 338 window.gtag = gtag; 339 gtag('js', new Date()); 340 gtag('config', 'G-WWJTNN1SFE'); 341 const script = document.createElement('script'); 342 script.async = true; 343 script.src = 'https://www.googletagmanager.com/gtag/js?id=G-WWJTNN1SFE'; 344 document.head.appendChild(script); 345 })(); 346</script>
346 347 348<style> 349 @import url('https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600&display=swap'); 350 351 :root { 352 /* Onboarding Black Glass - optimized for dark mode */ 353 --badge-black: rgba(0, 0, 0, 0.75); 354 --badge-black-hover: rgba(0, 0, 0, 0.85); 355 --badge-text: rgba(255, 255, 255, 0.9); 356 --badge-text-hover: rgba(255, 255, 255, 1); 357 --badge-border: rgba(255, 255, 255, 0.15); 358 --badge-border-hover: rgba(255, 255, 255, 0.25); 359 --badge-shadow: rgba(0, 0, 0, 0.4); 360 361 /* Rainbow gradient colors from Taskade Upgrade button */ 362 --rainbow-color-1: #5cb2f9; /* Blue */ 363 --rainbow-color-2: #8787f7; /* Purple-Blue */ 364 --rainbow-color-3: #a774f7; /* Purple */ 365 --rainbow-color-4: #e376a8; /* Pink */ 366 --rainbow-color-5: #eb7551; /* Orange */ 367 --rainbow-color-6: #19bd59; /* Green */ 368 --rainbow-color-7: #dee325; /* Yellow-Green */ 369 --rainbow-color-8: #e32545; /* Red */ 370 371 /* Cinematic easing */ 372 --ease-cinematic: cubic-bezier(0.22, 1, 0.36, 1); 373 --ease-cinematic-slow: cubic-bezier(0.16, 1, 0.3, 1); 374 } 375 376 [data-theme="light"] { 377 --badge-black: rgba(0, 0, 0, 0.8); 378 --badge-black-hover: rgba(0, 0, 0, 0.9); 379 --badge-text: rgba(255, 255, 255, 0.95); 380 --badge-text-hover: rgba(255, 255, 255, 1); 381 --badge-border: rgba(255, 255, 255, 0.2); 382 --badge-border-hover: rgba(255, 255, 255, 0.3); 383 --badge-shadow: rgba(0, 0, 0, 0.5); 384 } 385 386 @media (prefers-color-scheme: light) { 387 :root { 388 --badge-black: rgba(0, 0, 0, 0.8); 389 --badge-black-hover: rgba(0, 0, 0, 0.9); 390 --badge-text: rgba(255, 255, 255, 0.95); 391 --badge-text-hover: rgba(255, 255, 255, 1); 392 --badge-border: rgba(255, 255, 255, 0.2); 393 --badge-border-hover: rgba(255, 255, 255, 0.3); 394 --badge-shadow: rgba(0, 0, 0, 0.5); 395 } 396 } 397 398 /* Onboarding Black Glass Badge - Bottom Center Position */ 399 #taskade-badge { 400 position: fixed; 401 bottom: 24px; 402 left: 50%; 403 transform: translateX(-50%) scale(0.9); 404 z-index: 10001; 405 font-family: 'Geist', -apple-system, BlinkMacSystemFont, sans-serif; 406 animation: badgeGeistFadeIn 1.2s var(--ease-cinematic) forwards; 407 opacity: 0; 408 display: inline-flex; 409 align-items: center; 410 gap: 7px; 411 padding: 7px 12px; 412 font-size: 0.8125rem; 413 font-weight: 500; 414 letter-spacing: -0.01em; 415 line-height: 1; 416 color: var(--badge-text); 417 text-decoration: none; 418 background: var(--badge-black); 419 border: 1px solid var(--badge-border); 420 border-radius: 9999px; 421 backdrop-filter: blur(20px) saturate(1.4); 422 -webkit-backdrop-filter: blur(20px) saturate(1.4); 423 cursor: pointer; 424 transition: all 0.3s var(--ease-cinematic); 425 overflow: hidden; 426 will-change: transform, box-shadow; 427 white-space: nowrap; 428 } 429 430 #taskade-badge a { 431 display: flex; 432 align-items: center; 433 gap: 7px; 434 color: inherit; 435 text-decoration: none; 436 } 437 438 /* Default state with subtle rainbow glow */ 439 #taskade-badge { 440 box-shadow: 441 0 4px 16px var(--badge-shadow), 442 0 0 8px rgba(227, 118, 168, 0.2), 443 0 0 16px rgba(163, 116, 247, 0.15); 444 } 445 446 /* Black glass hover with enhanced rainbow glow */ 447 #taskade-badge:hover { 448 background: var(--badge-black-hover); 449 border-color: var(--badge-border-hover); 450 color: var(--badge-text-hover); 451 box-shadow: 452 0 8px 24px var(--badge-shadow), 453 0 0 16px rgba(227, 118, 168, 0.4), 454 0 0 32px rgba(163, 116, 247, 0.3), 455 inset 0 1px 0 rgba(255, 255, 255, 0.12); 456 transform: translateY(-3px) scale(1.03); 457 } 458 459 /* Taskade sheep mascot - optimized for dark mode */ 460 #taskade-badge #taskade-logo { 461 width: 16px; 462 height: 16px; 463 flex-shrink: 0; 464 opacity: 0.95; 465 transition: all 0.3s var(--ease-cinematic); 466 filter: invert(1) brightness(1.1) contrast(1.05); 467 } 468 469 #taskade-badge:hover #taskade-logo { 470 opacity: 1; 471 transform: scale(1.08) rotate(-2deg); 472 filter: invert(1) brightness(1.2) contrast(1.1); 473 } 474 475 /* Light mode adjustments */ 476 [data-theme="light"] #taskade-badge #taskade-logo { 477 filter: invert(0) brightness(1.1); 478 } 479 480 [data-theme="light"] #taskade-badge:hover #taskade-logo { 481 filter: invert(0) brightness(1.2); 482 } 483 484 @media (prefers-color-scheme: light) { 485 :root #taskade-badge #taskade-logo { 486 filter: invert(0) brightness(1.1); 487 } 488 489 :root #taskade-badge:hover #taskade-logo { 490 filter: invert(0) brightness(1.2); 491 } 492 } 493 494 /* Geist typography */ 495 #taskade-badge .badge-text { 496 font-weight: 500; 497 letter-spacing: 0.005em; 498 transition: all 0.3s var(--ease-cinematic); 499 -webkit-font-smoothing: antialiased; 500 -moz-osx-font-smoothing: grayscale; 501 } 502 503 #taskade-badge.hidden { 504 display: none; 505 } 506 507 /* Geist fade-in animation */ 508 @keyframes badgeGeistFadeIn { 509 0% { 510 opacity: 0; 511 transform: translateX(-50%) translateY(8px) scale(0.85); 512 filter: blur(6px); 513 } 514 60% { 515 opacity: 0.7; 516 filter: blur(2px); 517 } 518 100% { 519 opacity: 1; 520 transform: translateX(-50%) translateY(0) scale(0.9); 521 filter: blur(0); 522 } 523 } 524 525 /* Responsive */ 526 @media (max-width: 768px) { 527 #taskade-badge { 528 bottom: 80px; /* Above mobile nav/TAA chat */ 529 left: 50%; 530 transform: translateX(-50%) scale(0.85); 531 padding: 6px 10px; 532 font-size: 0.75rem; 533 gap: 5px; 534 } 535 536 #taskade-badge #taskade-logo { 537 width: 14px; 538 height: 14px; 539 } 540 } 541 542 /* Reduced motion */ 543 @media (prefers-reduced-motion: reduce) { 544 #taskade-badge { 545 animation: none; 546 opacity: 1; 547 } 548 549 #taskade-badge a, 550 #taskade-badge #taskade-logo, 551 #taskade-badge .badge-text { 552 transition: none; 553 animation: none; 554 } 555 } 556 557 /* High contrast */ 558 @media (prefers-contrast: high) { 559 #taskade-badge a { 560 border-width: 2px; 561 border-color: currentColor; 562 } 563 } 564 565 /* Hide when printing */ 566 @media print { 567 #taskade-badge { 568 display: none !important; 569 } 570 } 571</style> 572 573<div id="taskade-badge" data-badge-mode="edit"> 574 <a href="https://www.taskade.com/app/w0xpsigg24f3c3x5?utm_source=genesis_badge&utm_medium=app_badge&utm_campaign=p
574owered_by&utm_content=w0xpsigg24f3c3x5" target="_blank" rel="noopener noreferrer"> 575 <img id="taskade-logo" src="./_assets/logo.png" alt="Taskade" /> 576 <span class="badge-text" data-community-text="Edit in Taskade" data-default-text="Powered by Taskade">Edit in Taskade</span> 577 </a> 578</div> 579
580<script> 581 (function() { 582 // Geist theme detection - matching wait.html 583 function applyTheme() { 584 const urlParams = new URLSearchParams(window.location.search); 585 let theme = urlParams.get('theme'); 586 587 if (!theme) { 588 try { 589 if (window.parent !== window) { 590 const parentTheme = window.parent.document.documentElement.getAttribute('data-theme'); 591 if (parentTheme) theme = parentTheme; 592 } 593 } catch (e) {} 594 595 if (!theme) { 596 theme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; 597 } 598 } 599 600 document.documentElement.setAttribute('data-theme', theme === 'light' ? 'light' : 'dark'); 601 } 602 603 // Apply theme immediately 604 applyTheme(); 605 606 // Theme change listener 607 window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => { 608 const urlParams = new URLSearchParams(window.location.search); 609 if (!urlParams.get('theme')) { 610 document.documentElement.setAttribute('data-theme', e.matches ? 'light' : 'dark'); 611 } 612 }); 613 614 // Analytics tracking and postMessage communication 615 const badgeLink = document.querySelector('#taskade-badge a'); 616 if (badgeLink) { 617 let parentOrigin = '*'; 618 let parentHostname = ''; 619 if (document.referrer) { 620 try { 621 const referrerUrl = new URL(document.referrer); 622 parentOrigin = referrerUrl.origin; 623 parentHostname = referrerUrl.hostname; 624 } catch (err) { 625 parentOrigin = '*'; 626 parentHostname = ''; 627 } 628 } 629 // Taskade itself is framing us (the share page at /app/{id}, which 630 // iframes the app). parentOrigin stays '*' when there is no usable 631 // referrer, and then the framer is unknowable. 632 const framedByTaskade = 633 parentOrigin !== '*' && 634 (parentHostname === 'taskade.com' || parentHostname.endsWith('.taskade.com')); 635 636 const badge = document.getElementById('taskade-badge'); 637 const badgeText = document.querySelector('#taskade-badge .badge-text'); 638 const communityText = badgeText ? badgeText.getAttribute('data-community-text') : null; 639 const badgeHref = badgeLink.getAttribute('href') || ''; 640 const badgeMode = badge ? badge.getAttribute('data-badge-mode') : null; 641 const isCommunityShare = 642 badgeMode === 'edit' || 643 (badgeText && 644 communityText && 645 badgeText.textContent && 646 badgeText.textContent.trim() === communityText) || 647 badgeHref.indexOf('/app/') !== -1; 648 649 if (window.parent !== window) { 650 // Hide only when embedded: on the standalone domain this badge IS the 651 // viral return path into Taskade, so it must stay visible there. 652 // Scope note, two independent reasons to hide inside a frame: 653 // 1. The Taskade share page frames the app and already renders its own 654 // Clone CTA, so ANY badge mode is a duplicate there. A buyer of a 655 // non-template listing stays on /app/{id} and sees exactly that. 656 // 2. An edit-mode badge hides in any frame, because its /app/{id} link 657 // just points back at the framing page. 658 // A powered_by badge in an unrelated third-party embed still renders - 659 // there it is the only way back to Taskade. With no referrer the 660 // framer is unknowable, so only the mode-based reason applies. 661 if ((isCommunityShare || framedByTaskade) && badge) { 662 badge.classList.add('hidden'); 663 } 664 665 const notifyParent = () => { 666 window.parent.postMessage( 667 { 668 type: 'TASKADE_BADGE_MODE', 669 source: 'taskade_badge', 670 mode: isCommunityShare ? 'edit' : 'powered_by', 671 }, 672 parentOrigin 673 ); 674 }; 675 676 try { 677 notifyParent(); 678 setTimeout(notifyParent, 500); 679 } catch (err) { 680 // Ignore postMessage failures. 681 } 682 } 683 684 badgeLink.addEventListener('click', function() { 685 // Track badge engagement 686 if (typeof window.gtag === 'function') { 687 window.gtag('event', 'genesis_badge_clicked', { 688 event_category: 'engagement', 689 event_label: 'edit_in_taskade' 690 }); 691 } 692 693 }); 694 } 695 696 })(); 697</script>
vendor: 1 bytes, line 697
697
698<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"8d4254f20c524f6fbf6ecde0b2c89fb4","spa":2}' crossorigin="anonymous"></script>
698 699</body> 700</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.