PageSourceSearch

https://streamsnip.com/login?next=/

html streamsnip.com collected 2026-10-02 01:44:44 UTC 110,723 bytes, 2,439 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3
4<head>
5  
5<script>
6    // View Transitions reject .ready/.finished when a transition is interrupted (fast
7    // navigation, tab hidden, viewport resized). These are expected and benign, but show
8    // up as noisy unhandled rejections (AbortError / InvalidStateError) in error tracking.
9    // Swallow only those, tightly scoped to transition-related DOMExceptions.
10    window.addEventListener('unhandledrejection', function (e) {
11      var r = e.reason;
12      if (r && (r.name === 'AbortError' || r.name === 'InvalidStateError')
13            && typeof r.message === 'string' && /transition/i.test(r.message)) {
14        e.preventDefault();
15      }
16    });
17
18    // Direction-aware cross-document page transitions. Tag <html> so CSS slides content
19    // in from the right on forward navigations and from the left when going back/forward
20    // through history. Registered before first paint so it catches the pagereveal event.
21    window.addEventListener('pagereveal', function (e) {
22      if (!e.viewTransition) return;
23      var dir = 'forward';
24      try {
25        var act = window.navigation && window.navigation.activation;
26        if (act) {
27          if (act.navigationType === 'reload') { dir = null; }
28          else if (act.navigationType === 'traverse' && act.from && act.entry) {
29            dir = act.entry.index < act.from.index ? 'back' : 'forward';
30          }
31        }
32      } catch (_) {}
33      var root = document.documentElement;
34      root.classList.remove('vt-forward', 'vt-back');
35      if (dir) root.classList.add(dir === 'back' ? 'vt-back' : 'vt-forward');
36      e.viewTransition.finished.finally(function () {
37        root.classList.remove('vt-forward', 'vt-back');
38      });
39    });
40  </script>
40
41  
41<script>
42    // Set theme synchronously before first paint to avoid FOUC. The class is mirrored onto <body>
43    // by an inline script at the top of <body> so `body.dark-theme` selectors still match.
44    (function(){var d=localStorage.getItem('darkTheme');var dark=d?d==='true':window.matchMedia('(prefers-color-scheme: dark)').matches;if(dark)document.documentElement.classList.add('dark-theme');})();
45    // Palette choice — re-apply the accent chosen via the navbar palette dropdown, before first paint.
46    (function(){try{var p=JSON.parse(localStorage.getItem('ss_palette')||'null');if(p&&p.h){var s=document.documentElement.style;s.setProperty('--accent-h',p.h);s.setProperty('--accent-s',p.s);s.setProperty('--accent-l',p.l);if(p.rgb)s.setProperty('--color-primary-rgb',p.rgb);}}catch(e){}})();
47  </script>
47
48  
48<script>
49    (function () {
50      function isWeakGPU(renderer) {
51        if (!renderer || renderer === 'Unknown') return false;
52        var r = renderer.toLowerCase();
53        // Intel integrated
54        if (r.indexOf('intel') !== -1 && (
55          r.indexOf('hd graphics') !== -1 ||
56          r.indexOf('uhd graphics') !== -1 ||
57          r.indexOf('iris') !== -1
58        )) return true;
59        // AMD APU integrated (no discrete RX/Pro)
60        if (r.indexOf('amd') !== -1 && r.indexOf('radeon') !== -1 &&
61          (r.indexOf('vega') !== -1 || r.indexOf('graphics') !== -1) &&
62          r.indexOf(' rx ') === -1 && r.indexOf(' pro ') === -1) return true;
63        // Software renderers
64        if (r.indexOf('microsoft basic render') !== -1 ||
65          r.indexOf('swiftshader') !== -1 ||
66          r.indexOf('llvmpipe') !== -1) return true;
67        return false;
68      }
69      try {
70        var c = document.createElement('canvas');
71        var gl = c.getContext('webgl2') || c.getContext('webgl') || c.getContext('experimental-webgl');
72        if (!gl) {
73          document.documentElement.classList.add('no-gpu');
74        } else {
75          var dbg = gl.getExtension('WEBGL_debug_renderer_info');
76          var renderer = dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : '';
77          if (isWeakGPU(renderer)) document.documentElement.classList.add('no-gpu');
78        }
79      } catch (e) { document.documentElement.classList.add('no-gpu'); }
80    })();
81  </script>
81
82  <!-- ========= SENTRY ========= -->
83  
83<script src="https://browser.sentry-cdn.com/9.18.0/bundle.tracing.replay.feedback.min.js"
84    integrity="sha384-AjEie096M0si7Z7evQkKpGRBFXtrRj0v7pb1FTQau13fhSYTdtEgfW85kTIrWdBe"
85    crossorigin="anonymous"></script>
85
86  
86<script>
87    // Guard against the CDN script being blocked (ad blockers, offline, integrity failure)
88    if (typeof Sentry !== 'undefined') {
89      Sentry.init({
90        dsn: "https://05d1a950ad00f7142bea03c3e956de46@o4509799317897216.ingest.us.sentry.io/4509799330152448",
91        integrations: [
92          Sentry.browserTracingIntegration(),
93          Sentry.replayIntegration({
94            maskAllText: false,
95            blockAllMedia: false,
96          }),
97          Sentry.feedbackIntegration({
98            colorScheme: "dark",
99            // Don't inject the floating widget; attach to the navbar button instead
100            // so it can never overlap the download-queue panel.
101            autoInject: false,
102          }),
103        ],
104        // Anchored to our own origin so the trace header is never attached to
105        // third-party requests (e.g. Beamer) whose URLs merely contain our
106        // domain as a query param — that triggers a CORS preflight rejection.
107        tracePropagationTargets: [/^https:\/\/(www\.)?streamsnip\.com/, /^\//],
108        // Sampled down from 1.0/0.1/1.0: replay's DOM recorder and tracing ran for
109        // every visitor (error replay buffers even on unsampled sessions), adding
110        // measurable main-thread overhead. ~90% of sessions now record nothing.
111        tracesSampleRate: 0.1,
112        replaysSessionSampleRate: 0.01,
113        replaysOnErrorSampleRate: 0.1,
114        // Crashes thrown entirely inside a visitor's browser extension are not our
115        // bugs and we can't fix them — keep them out of the issue stream.
116        denyUrls: [/^chrome-extension:\/\//i, /^moz-extension:\/\//i, /^safari-web-extension:\/\//i],
117        beforeSend(event, hint) {
118          // View Transitions reject .ready/.finished when interrupted (fast nav, tab hidden,
119          // viewport resized), surfacing as benign AbortError/InvalidStateError unhandled
120          // rejections. Sentry's own unhandledrejection handler captures these regardless of
121          // the global preventDefault() handler, so they have to be dropped here at the SDK level.
122          var ex = hint && hint.originalException;
123          if (ex && (ex.name === 'AbortError' || ex.name === 'InvalidStateError')
124                && typeof ex.message === 'string' && /transition/i.test(ex.message)) {
125            return null;
126          }
127          
128          return event;
129        },
130      });
131      // The trigger lives in the navbar (rendered later in <body>), so wait for the DOM.
132      document.addEventListener('DOMContentLoaded', function () {
133        const btn = document.getElementById('report-bug-btn');
134        if (btn) Sentry.getFeedback()?.attachTo(btn, { formTitle: "Report a bug" });
135      });
136    }
137  </script>
137
138
139  <!-- ========= GOOGLE ANALYTICS (GA4) ========= -->
140  
141  <!-- No SRI: Google serves gtag.js dynamically per id and updates it often, so a pinned hash would break it. -->
vendor: 66 bytes, lines 141-142
141
142  <script async src="https://www.googletagmanager.com/gtag/js?id=
142G-1W58W85E8M
vendor: 14 bytes, lines 142-143
142"></script>
143  
143<script>
144    
vendor: 139 bytes, lines 144-147
144window.dataLayer = window.dataLayer || [];
145    function gtag(){dataLayer.push(arguments);}
146    gtag('js', new Date());
147    gtag('config', "
147G-1W58W85E8M
vendor: 6 bytes, lines 147-148
147");
148  
148</script>
148
149  
150
151  <!-- ========= META & SEO ========= -->
152  <meta charset="utf-8" />
153  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
154  
155  <title>Login to StreamSnip</title>
156  <meta name="description"
157    content="Log in to your StreamSnip account to manage your clips, view channel statistics, and update your settings." />
158  <meta name="keywords"
159    content="Livestream, YouTube, StreamSnip, !clip, Nightbot, Nightbot-clip, YouTube Clipping, Stream Moments" />
160  <meta name="author" content="Suraj Bhari" />
161  <meta name="copyright" content="© 2026 Suraj Bhari. All rights reserved." />
162  
163  <meta name="robots"
164    content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1, noai, noimageai" />
165  <link rel="canonical" href="https://streamsnip.com/login" />
166  <link rel="manifest" href="/static/manifest.json" />
167  <meta name="theme-color" content="#0080ff" />
168  <meta name="mobile-web-app-capable" content="yes" />
169  <meta name="apple-mobile-web-app-capable" content="yes" />
170  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
171  <meta name="apple-mobile-web-app-title" content="StreamSnip" />
172
173  <!-- Open Graph / Facebook -->
174  <meta property="og:type" content="website" />
175  <meta property="og:url" content="https://streamsnip.com/login" />
176  <meta property="og:title" content="Login to StreamSnip" />
177  <meta property="og:description" content="Log in to your StreamSnip account to manage your clips, view channel statistics, and update your settings." />
178  <meta property="og:image"
179    content="https://streamsnip.com/static/logo.png?v=7142fd7189" />
180  <meta property="og:image:alt" content="Login to StreamSnip" />
181  <meta property="og:site_name" content="StreamSnip" />
182  <meta property="og:locale" content="en_US" />
183
184  <!-- Twitter -->
185  <meta name="twitter:card" content="summary_large_image" />
186  <meta name="twitter:url" content="https://streamsnip.com/login" />
187  <meta name="twitter:title" content="Login to StreamSnip" />
188  <meta name="twitter:description" content="Log in to your StreamSnip account to manage your clips, view channel statistics, and update your settings." />
189  <meta name="twitter:image" content="https://streamsnip.com/static/logo.png?v=7142fd7189" />
190  <meta name="twitter:image:alt" content="Login to StreamSnip" />
191
192  <!-- Favicon -->
193  <link rel="icon" type="image/png" href="/static/logo.png?v=7142fd7189" />
194  <link rel="apple-touch-icon" href="/static/logo.png?v=7142fd7189" />
195
196  <!-- ========= STYLES & CRITICAL ASSETS PRELOAD ========= -->
197  <link rel="dns-prefetch" href="https://fonts.googleapis.com" />
198  <link rel="dns-prefetch" href="https://fonts.gstatic.com" />
199  <link rel="dns-prefetch" href="https://use.fontawesome.com" />
200  <link rel="dns-prefetch" href="https://cdn.jsdelivr.net" />
201
202  <link rel="preconnect" href="https://fonts.googleapis.com" />
203  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
204  <link rel="preconnect" href="https://use.fontawesome.com" crossorigin />
205  <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
206
207  <link rel="preload" as="style" href="/static/style.css?v=2632f62682" />
208  <link rel="preload" as="style"
209    href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=Poppins:wght@400;600&family=Rajdhani:wght@600;700&display=swap" />
210  <link rel="preload" as="image" href="/static/logo-grey.svg" />
211  <link rel="preload" as="image" href="/static/logo.svg" />
212
213  <!-- FontAwesome is icons only (not layout) — load it off the render-blocking path so the
214       page content paints without waiting on this CDN request. Falls back to a blocking
215       load with JS disabled. -->
216  <link rel="preload" as="style" href="https://use.fontawesome.com/releases/v6.5.1/css/all.css"
217    onload="this.onload=null;this.rel='stylesheet'" />
218  <noscript>
219    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v6.5.1/css/all.css" />
220  </noscript>
221  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
222    integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous" />
223  <link
224    href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;
224800&family=Poppins:wght@400;600&family=Rajdhani:wght@600;700&display=swap"
225    rel="stylesheet" />
226
227  <link rel="stylesheet" href="/static/style.css?v=2632f62682" />
228
229  <style>
230    body {
231      -webkit-font-smoothing: antialiased;
232      -moz-osx-font-smoothing: grayscale;
233    }
234
235    .btn:active,
236    .nav-link:active {
237      transform: scale(0.96) !important;
238      transition: transform 0.1s cubic-bezier(0.4, 0, 0.2, 1) !important;
239    }
240
241    h1,
242    h2,
243    h3,
244    h4,
245    h5,
246    h6 {
247      text-wrap: balance;
248    }
249
250    p {
251      text-wrap: pretty;
252    }
253
254    .profile-frame img {
255      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
256    }
257
258    .dark-theme .profile-frame img {
259      box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
260    }
261
262    .site_icon,
263    #site_icon,
264    .site-icon,
265    .navbar-brand,
266    .navbar-brand img,
267    .navbar-brand:focus,
268    .navbar-brand:active {
269      box-shadow: none !important;
270      outline: none !important;
271      border: none !important;
272    }
273
274    /* Navbar clapper logo */
275    #_nbl_stick, #_nbl_clapper { fill: #444444; }
276    body.dark-theme #_nbl_stick, body.dark-theme #_nbl_clapper { fill: #fffcfc; }
277    #_nbl_svg { transform: rotate(17deg); transform-origin: center; }
278    @keyframes _nbl_impact {
279      0%   { transform: rotate(17deg) scale(1)    translateX(0px); }
280      51%  { transform: rotate(17deg) scale(1.07) translateX(0px); animation-timing-function: cubic-bezier(0.36,0.07,0.19,0.97); }
281      53%  { transform: rotate(17deg) scale(1.05) translateX(-5px); }
282      55%  { transform: rotate(17deg) scale(1.04) translateX(5px); }
283      57%  { transform: rotate(17deg) scale(1.02) translateX(-3px); }
284      59%  { transform: rotate(17deg) scale(1.01) translateX(2px); }
285      63%  { transform: rotate(17deg) scale(1)    translateX(0px); }
286      100% { transform: rotate(17deg) scale(1)    translateX(0px); }
287    }
288    #_nbl_svg._nbl_play { animation: _nbl_impact 2.6s linear 1 forwards; }
289
290    /* ===== Clapper loading animation: a looping version of the navbar logo's clap ===== */
291    /* Faithful loop of the navbar clap: ease-out open → HOLD open → snap shut →
292       bounce → hold closed → settle to rest → brief pause, then repeat. */
293    @keyframes ss-clap {
294      0%   { transform: rotate(-17deg); animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); }
295      16%  { transform: rotate(-57deg); animation-timing-function: linear; }
296      22%  { transform: rotate(-57deg); animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
297      40%  { transform: rotate(0deg);   animation-timing-function: cubic-bezier(0, 0, 0.3, 1); }
298      45%  { transform: rotate(-8deg);  animation-timing-function: cubic-bezier(0.25, 1, 0.5, 1); }
299      51%  { transform: rotate(0deg);   animation-timing-function: linear; }
300      62%  { transform: rotate(0deg);   animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); }
301      78%  { transform: rotate(-17deg); }
302      100% { transform: rotate(-17deg); }
303    }
304    /* Whole-board recoil: scale punch + damped horizontal shake, synced to the snap (40%). */
305    @keyframes ss-clap-impact {
306      0%    { transform: rotate(17deg) scale(1)    translateX(0); }
307      40%   { transform: rotate(17deg) scale(1.07) translateX(0);    animation-timing-function: cubic-bezier(0.36, 0.07, 0.19, 0.97); }
308      41.5% { transform: rotate(17deg) scale(1.05) translateX(-5px); }
309      43%   { transform: rotate(17deg) scale(1.04) translateX(5px); }
310      44.5% { transform: rotate(17deg) scale(1.02) translateX(-3px); }
311      46%   { transform: rotate(17deg) scale(1.01) translateX(2px); }
312      49%   { transform: rotate(17deg) scale(1)    translateX(0); }
313      100%  { transform: rotate(17deg) scale(1)    translateX(0); }
314    }
315    .ss-clap {
316      width: 48px; height: 48px; display: block; overflow: visible;
317      transform: rotate(17deg); transform-origin: center;
318      animation: ss-clap-impact 1.6s linear infinite;
319    }
320    .ss-clap .ss-clap-stick { transform-origin: 958px 614px; animation: ss-clap 1.6s linear infinite; }
321    .ss-clap .ss-clap-stick, .ss-clap .ss-clap-board { fill: #444444; transition: fill .3s ease; }
322    body.dark-theme .ss-clap .ss-clap-stick, body.dark-theme .ss-clap .ss-clap-board { fill: #fffcfc; }
323    @media (prefers-reduced-motion: reduce) {
324      .ss-clap, .ss-clap .ss-clap-stick { animation-duration: 2.4s; }
325    }
326    .clap-loader-box { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .85rem; }
327    .clap-loader-box.inline { flex-direction: row; gap: .55rem; }
328    .clap-loader-label { font-size: .9rem; color: var(--color-text-muted); font-weight: 500; }
329  </style>
330  <!-- ========= SPECULATION RULES =========
331       Make internal navigation feel instant, pairing with the cross-document View
332       Transitions above. `prefetch` only fetches the HTML (no script execution), so it
333       never triggers a page's analytics/view-tracking or YouTube loads early — safe to
334       apply broadly. `prerender` fully loads + runs the target, so it's limited to the
335       static informational pages where running their JS ahead of time has no side
336       effects. Auth/download/payment/API routes are excluded from both. `moderate`
337       eagerness fires on hover / pointerdown intent rather than eagerly on render. -->
338  
338<script type="speculationrules">
339  {
340    "prefetch": [{
341      "where": { "and": [
342        { "href_matches": "/*" },
343        { "not": { "href_matches": "/logout*" } },
344        { "not": { "href_matches": "/login*" } },
345        { "not": { "href_matches": "/download*" } },
346        { "not": { "href_matches": "/video/*" } },
347        { "not": { "href_matches": "/api/*" } },
348        { "not": { "href_matches": "/pay*" } },
349        { "not": { "href_matches": "/gift/*" } },
350        { "not": { "selector_matches": "[data-no-prefetch]" } },
351        { "not": { "selector_matches": "[target=_blank]" } }
352      ]},
353      "eagerness": "moderate"
354    }],
355    "prerender": [
356      
357      {
358      "where": { "or": [
359        { "href_matches": "/e/*" },
360        { "href_matches": "/exports/*" },
361        { "href_matches": "/get-started" },
362        { "href_matches": "/guides" },
363        { "href_matches": "/aboutus" },
364        { "href_matches": "/contactus" },
365        { "href_matches": "/privacy-policy" },
366        { "href_matches": "/tnc" },
367        { "href_matches": "/stats" },
368        { "href_matches": "/membership" },
369        { "href_matches": "/all_games" }
370      ]},
371      "eagerness": "moderate"
372    }]
373  }
374  </script>
374
375
376  <!-- ========= STRUCTURED DATA: Organization + WebSite (site-wide entity graph) ========= -->
377  
377<script type="application/ld+json">
378  {
379    "@context": "https://schema.org",
380    "@graph": [
381      {
382        "@type": "Organization",
383        "@id": "https://streamsnip.com/#organization",
384        "name": "StreamSnip",
385        "url": "https://streamsnip.com",
386        "logo": {
387          "@type": "ImageObject",
388          "url": "https://streamsnip.com/static/logo.png",
389          "width": 512,
390          "height": 512
391        },
392        "description": "StreamSnip is a freemium clipping tool for YouTube livestreamers. Capture highlight moments instantly with a single chat command.",
393        "foundingDate": "2023-01-24",
394        "founder": {
395          "@type": "Person",
396          "@id": "https://streamsnip.com/#founder",
397          "name": "Suraj Bhari"
398        },
399        "contactPoint": {
400          "@type": "ContactPoint",
401          "contactType": "customer support",
402          "email": "[email protected]",
403          "url": "https://streamsnip.com/contactus"
404        },
405        "sameAs": [
406          "https://discord.gg/2XVBWK99Vy"
407        ]
408      },
409      {
410        "@type": "WebSite",
411        "@id": "https://streamsnip.com/#website",
412        "url": "https://streamsnip.com",
413        "name": "StreamSnip",
414        "description": "Simplified YouTube livestream clipping for streamers and their communities.",
415        "publisher": {
416          "@id": "https://streamsnip.com/#organization"
417        }
418      }
419    ]
420  }
421  </script>
421
422
423  
424<style>
425    /* ── Full-page layout: push footer to bottom ── */
426    .login-page {
427        display: flex;
428        flex-direction: column;
429        flex: 1;
430        position: relative;
431        overflow: hidden;
432    }
433
434    /* ── Hero background (star warp canvas) ── */
435    .login-hero-bg {
436        position: fixed;
437        top: 0;
438        left: 0;
439        width: 100%;
440        height: 100%;
441        z-index: 0;
442        background: radial-gradient(circle at center, hsl(var(--accent-h) 36% 97%) 0%, var(--color-canvas) 100%);
443        transition: background 0.5s ease;
444    }
445
446    .dark-theme .login-hero-bg {
447        background: radial-gradient(circle at center, hsl(var(--accent-h) 30% 12%) 0%, var(--color-canvas) 100%);
448    }
449
450    #login-canvas {
451        position: absolute;
452        top: 0;
453        left: 0;
454        width: 100%;
455        height: 100%;
456        pointer-events: none;
457    }
458
459    /* ── Center the login card ── */
460    .login-wrapper {
461        flex: 1;
462        display: flex;
463        align-items: center;
464        justify-content: center;
465        padding: 2rem 1rem;
466        position: relative;
467        z-index: 2;
468    }
469    
470    .login-card {
471        width: 100%;
472        max-width: 420px;
473        padding: 2.5rem;
474        transition: transform var(--transition-base), box-shadow var(--transition-base);
475    }
476    
477    .login-card:hover {
478        transform: translateY(-4px);
479        box-shadow: 0 12px 40px 0 rgba(31, 38, 135, 0.18);
480    }
481
482    .login-header {
483        text-align: center;
484        margin-bottom: 2rem;
485    }
486
487    .login-header h1 {
488        font-family: 'Outfit', sans-serif;
489        font-weight: 700;
490        font-size: 2rem;
491        margin-bottom: 0;
492        color: var(--color-primary);
493        text-wrap: balance;
494    }
495
496    /* ── Modern Input ── */
497    .input-group-custom {
498        margin-bottom: 1.25rem;
499        position: relative;
500    }
501
502    .input-group-custom i {
503        position: absolute;
504        inset-inline-start: 1.25rem;
505        top: 50%;
506        transform: translateY(-50%);
507        color: var(--color-primary);
508        opacity: 0.6;
509        font-size: 0.9rem;
510    }
511
512    .glass-input {
513        width: 100%;
514        background: rgba(255, 255, 255, 0.05);
515        border: 1px solid var(--glass-border);
516        border-radius: var(--radius-md);
517        padding-block: 0.9rem;
518        padding-inline: 3.25rem 1rem;
519        color: var(--color-text);
520        font-size: 0.95rem;
521        transition: background-color var(--transition-base), border-color var(--transition-base), box-shadow var(--transition-base);
522    }
523
524    .glass-input::placeholder {
525        color: var(--color-text-muted);
526        opacity: 0.6;
527    }
528
529    .glass-input:focus {
530        background: rgba(255, 255, 255, 0.08);
531        border-color: var(--color-primary);
532        box-shadow: 0 0 0 3px var(--color-primary-glow);
533        outline: none;
534    }
535
536    /* ── Remember checkbox ── */
537    .remember-row {
538        display: flex;
539        align-items: center;
540        justify-content: space-between;
541        margin-bottom: 1.5rem;
542    }
543
544    .remember-label {
545        display: flex;
546        align-items: center;
547        gap: 0.5rem;
548        cursor: pointer;
549        user-select: none;
550        color: var(--color-text-muted);
551        font-size: 0.875rem;
552        margin: 0;
553    }
554
555    .remember-label input[type="checkbox"] {
556        accent-color: var(--color-primary);
557        width: 1rem;
558        height: 1rem;
559        cursor: pointer;
560    }
561
562    .msg-hint {
563        font-size: 0.8rem;
564        color: var(--color-text-muted);
565        text-align: center;
566        margin: 1.25rem 0 0;
567        line-height: 1.4;
568        opacity: 0.7;
569    }
570
571    /* ── Divider ── */
572    .divider {
573        display: flex;
574        align-items: center;
575        text-align: center;
576        margin: 1.75rem 0;
577        color: var(--color-text-muted);
578        font-size: 0.8rem;
579        text-transform: uppercase;
580        letter-spacing: 0.05em;
581    }
582
583    .divider::before, .divider::after {
584        content: '';
585        flex: 1;
586        border-bottom: 1px solid var(--glass-border);
587    }
588
589    .divider:not(:empty)::before { margin-inline-end: .75rem; }
590    .divider:not(:empty)::after { margin-inline-start: .75rem; }
591
592    /* ── Modern Google Button ── */
593    .google-btn {
594        display: flex;
595        align-items: center;
596        justify-content: center;
597        gap: 0.75rem;
598        width: 100%;
599        padding: 0.75rem 1.5rem;
600        border-radius: var(--radius-md);
601        border: 1px solid var(--glass-border);
602        background: rgba(255,255,255,0.05);
603        color: var(--color-text);
604        font-size: 0.95rem;
605        font-weight: 500;
606        text-decoration: none;
607        cursor: pointer;
608        transition: background-color var(--transition-base), border-color var(--transition-base), transform var(--transition-base), box-shadow var(--transition-base), color var(--transition-base);
609    }
610
611    .google-btn:hover {
612        background: rgba(255,255,255,0.12);
613        border-color: var(--color-primary);
614        transform: translateY(-1px);
615        box-shadow: 0 4px 12px rgba(0,0,0,0.1);
616        color: var(--color-text);
617        text-decoration: none;
618    }
619    
620    .google-btn:active {
621        transform: scale(0.96);
622    }
623    
624    .btn:active {
625        transform: scale(0.96);
626        transition: transform 0.1s ease;
627    }
628
629    .google-btn svg {
630        width: 20px;
631        height: 20px;
632        flex-shrink: 0;
633    }
634
635    .signup-hint {
636        text-align: center;
637        margin-top: 1.5rem;
638        font-size: 0.875rem;
639        color: var(--color-text-muted);
640    }
641
642    .signup-hint a {
643        color: var(--color-primary);
644        font-weight: 600;
645        text-decoration: none;
646    }
647
648    .signup-hint a:hover {
649        text-decoration: underline;
650    }
651</style>
652
653</head>
654
655<body>
656  
656<script>
657    // Mirror the early <html> dark-theme class onto <body> so existing `body.dark-theme` rules apply before first paint.
658    if (document.documentElement.classList.contains('dark-theme')) document.body.classList.add('dark-theme');
659  </script>
659
660
661  <!-- ssIsTypingTarget / ssRegisterShortcuts. Not deferred: page scripts that bind
662       shortcuts at parse time call into it. -->
663  
663<script src="/static/js/shortcuts.js?v=c98906de16"></script>
663
664  <!-- ssConfirm — themed replacement for window.confirm(). -->
665  
665<script src="/static/js/confirm.js?v=8a2fd43037" defer></script>
665
666
667  <!-- ===== Reusable clapper loader (single source of truth) =====
668       Server-rendered: <div data-clap-loader data-size="48" data-label="…"></div> (auto-hydrated).
669       JS-injected:     window.clapLoaderHTML({size, label, inline}) / window.makeClapLoader(...). -->
670  <template id="ss-clap-tpl">
671    <svg class="ss-clap" viewBox="0 0 3334 3334" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
672      <path class="ss-clap-stick" fill-rule="evenodd" d="M 2630.692 408.528 C 2630.622 408.998 2932.538 739.198 2932.538 739.198 L 2935.557 411.334 M 2329.824 405.758 C 2329.564 405.972 1890.172 401.018 1890.292 401.711 C 1890.412 402.336 2203.145 732.254 2205.705 732.506 C 2207.355 732.67 2629.269 736.133 2629.769 735.802 M 1532.807 398.421 C 1532.557 398.615 1083.58 393.355 1083.68 394.286 C 1083.76 395.084 1399.75 724.278 1400.29 724.803 C 1400.82 725.329 1835.934 729.102 1835.931 729.103 M 471.788 388.873 L 468.835 716.516 C 468.835 716.516 1089.13 725.939 1089.05 724.765 C 1088.98 723.601 779.835 391.763 776.285 391.456"/>
673      <path class="ss-clap-board" fill-rule="evenodd"
674        transform="matrix(0.95517403, -0.29604402, 0.29604402, 0.95517403, -456.95786825, 588.0155987)"
675        d="M 524.433 1432.53 C 365.242 1947.64 234.251 2372.63 233.351 2376.95 C 232.451 2381.27 231.481 2389.75 231.191 2395.8 C 230.841 2403.26 231.181 2410.12 232.261 2417.13 C 233.131 2422.81 235.451 2432.26 237.411 2438.13 C 239.381 2444 242.971 2452.44 245.401 2456.9 C 247.831 2461.35 251.911 2467.88 254.481 2471.4 C 257.041 2474.92 262.871 2481.46 267.431 2485.94 C 271.991 2490.42 279.241 2496.41 283.551 2499.25 C 287.851 2502.1 295.001 2506.17 299.451 2508.31 C 305.091 2511.02 623.563 2609.86 1356.05 2836.23 C 1932.73 3014.45 2408.35 3161.15 2412.97 3162.23 C 2417.59 3163.32 2425.42 3164.74 2430.37 3165.38 C 2436.04 3166.13 2445.47 3166.35 2455.87 3165.98 C 2466.63 3165.6 2475.74 3164.68 2482.05 3163.34 C 2487.37 3162.21 2495.92 3159.92 2501.05 3158.25 C 2506.18 3156.57 2515.08 3152.96 2520.84 3150.21 C 2526.6 3147.46 2535.6 3142.35 2540.85 3138.86 C 2546.09 3135.37 2554.2 3129.17 2558.87 3125.08 C 2563.55 3121 2570.58 3113.86 2574.51 3109.23 C 2578.43 3104.59 2584.16 3097.03 2587.23 3092.43 C 2590.31 3087.83 2595.04 3079.5 2597.75 3073.93 C 2600.47 3068.36 2604.3 3058.85 2606.28 3052.8 C 2608.26 3046.75 2741.7 2615.21 2902.81 2093.83 C 3063.93 1572.44 3198.395 1137.842 3198.215 1137.672 C 3198.045 1137.492 2817.49 1343.35 2817.49 1343.35 C 2817.49 1343.35 2528.38 1254.14 2528.24 1254 C 2528.1 1253.86 2909.86 1047.81 2908.62 1047.27 C 2907.38 1046.73 2503.85 922.031 2503.22 921.811 C 2502.59 921.601 2109.55 1124.54 2109.55 1124.54 C 2109.55 1124.54 1767.13 1018.87 1765.62 1018.58 C 1764.11 1018.3 2151.16 813.221 2150.17 812.831 C 2149.18 812.441 1730.94 683.301 1730.12 683.301 C 1729.31 683.301 1363.89 871.901 1354.74 876.651 C 1345.59 881.401 1337.18 885.291 1336.04 885.291 C 1334.9 885.291 1062.03 800.871 1055.5 798.831 C 1048.97 796.791 1044.03 794.771 1044.5 794.341 C 1044.98 793.921 1433.417 591.777 1433.197 590.627 C 1432.977 589.477 881.045 419.444 844.455 408.154 M 943.532 525.271 C 940.872 525.761 935.492 527.121 931.582 528.311 C 927.672 529.501 921.022 532.201 916.812 534.321 C 912.602 536.441 905.722 540.901 901.522 544.241 C 897.322 547.581 891.112 553.791 887.712 558.051 C 884.182 562.481 879.512 570.071 876.812 575.791 C 874.201 581.291 871.321 588.721 870.391 592.291 C 869.471 595.871 868.471 603.971 868.181 610.291 C 867.781 618.801 868.071 624.131 869.331 630.771 C 870.261 635.701 872.341 643.171 873.961 647.361 C 875.572 651.551 879.542 658.981 882.772 663.891 C 886.012 668.781 891.532 675.561 895.052 678.931 C 898.582 682.311 904.952 687.341 909.212 690.131 C 913.472 692.911 920.422 696.521 924.662 698.151 C 928.902 699.781 935.842 701.831 940.082 702.711 C 944.322 703.581 952.322 704.301 957.872 704.301 C 963.422 704.301 971.422 703.581 975.662 702.711 C 979.902 701.831 986.842 699.781 991.082 698.151 C 995.322 696.521 1002.27 692.911 1006.53 690.131 C 1010.79 687.341 1017.16 682.311 1020.67 678.931 C 1024.19 675.561 1029.48 669.171 1032.43 664.741 C 1035.38 660.311 1039.22 653.341 104
6750.96 649.241 C 1042.7 645.151 1044.96 637.741 1045.99 632.791 C 1047.01 627.841 1047.85 619.971 1047.85 615.291 C 1047.85 610.621 1047.39 603.871 1046.83 600.291 C 1046.26 596.721 1044.86 590.871 1043.7 587.291 C 1042.54 583.721 1039.88 577.371 1037.79 573.181 C 1035.7 569.001 1031.26 562.141 1027.92 557.941 C 1024.59 553.741 1018.58 547.701 1014.58 544.521 C 1010.58 541.341 1003.7 536.861 999.292 534.571 C 994.882 532.281 987.242 529.211 982.322 527.761 C 975.302 525.691 970.682 525.041 960.872 524.761 C 953.992 524.561 946.192 524.791 943.532 525.271 M 1097.12 1239.09 C 964.982 1320.88 856.881 1388.02 856.881 1388.3 C 856.891 1388.57 1003.82 1573.48 1183.41 1799.2 L 1509.93 2209.59 L 1427.65 2264.44 L 1345.37 2319.28 L 1152.07 2252.4 C 1045.75 2215.62 958.492 2185.25 958.152 2184.91 C 957.812 2184.57 957.722 2184.15 957.952 2183.96 C 958.182 2183.78 1016.2 2150.17 1086.87 2109.29 C 1157.55 2068.4 1216.6 2034.2 1218.11 2033.29 L 1220.84 2031.64 L 1145.29 1932.72 C 1103.73 1878.31 1069.31 1833.91 1068.79 1834.05 C 1068.26 1834.2 914.962 1929.76 728.101 2046.42 C 541.253 2163.08 387.822 2259.04 387.162 2259.66 C 386.242 2260.52 508.042 2300.95 886.112 2425.3 C 1161.21 2515.77 1386.88 2589.8 1387.61 2589.8 C 1388.34 2589.8 1495.56 2520.05 1625.87 2434.8 C 1759.06 2347.67 1862.63 2279.29 1862.4 2278.63 C 1862.18 2277.99 1715.65 2093.49 1536.79 1868.63 C 1357.93 1643.77 1211.2 1459.31 1210.74 1458.71 C 1210.18 1457.99 1237.69 1439.08 1292.07 1402.83 L 1374.26 1348.04 L 1468.67 1372.7 L 1563.09 1397.36 L 1687.73 1320.88 C 1756.28 1278.81 1813.17 1243.81 1814.16 1243.08 C 1815.7 1241.95 1784.17 1231.67 1577.66 1166 C 1446.6 1124.32 1338.92 1090.25 1338.37 1090.29 C 1337.82 1090.33 1229.26 1157.29 1097.12 1239.09 M 1717.29 1441.36 C 1586.98 1526.65 1480.26 1596.74 1480.14 1597.11 C 1480.01 1597.49 1626.84 1782.47 1806.43 2008.19 L 2132.96 2418.58 L 2050.66 2473.42 L 1968.37 2528.27 L 1874.5 2503.73 C 1822.87 2490.24 1780.1 2479.33 1779.45 2479.5 C 1778.8 2479.66 1721.93 2514.38 1653.07 2556.65 C 1584.21 2598.92 1527.87 2633.8 1527.87 2634.17 C 1527.87 2634.54 1635.09 2668.96 1766.14 2710.66 L 2004.42 2786.47 L 2245.1 2637.64 C 2388.34 2549.07 2485.63 2488.31 2485.38 2487.59 C 2485.15 2486.93 2338.63 2302.43 2159.78 2077.59 C 1980.93 1852.76 1834.22 1668.31 1833.75 1667.71 C 1833.18 1666.98 1860.5 1648.21 1915.06 1611.84 L 1997.23 1557.05 L 2012.3 1562.22 C 2020.59 1565.06 2107.62 1595.15 2205.7 1629.09 C 2303.78 1663.03 2384.03 1691.25 2384.02 1691.8 C 2384.02 1692.35 2325.36 1726.77 2253.66 1768.3 C 2181.96 1809.82 2123.09 1844.11 2122.84 1844.5 C 2122.58 1844.88 2156.34 1889.71 2197.87 1944.11 L 2273.37 2043.01 L 2279.37 2039.25 C 2282.67 2037.17 2436.12 1941.35 2620.37 1826.31 C 2804.62 1711.26 2955.75 1616.77 2956.21 1616.33 C 2956.67 1615.89 2952.84 1614.14 2947.71 1612.45 C 2942.57 1610.76 2717.3 1536.69 2447.11 1447.84 C 2176.92 1358.99 1955.48 1286.3 1955.03 1286.3 C 1954.57 1286.3 1847.59 1356.08 1717.29 1441.36"/>
676    </svg>
677  </template>
678  
678<script>
679    (function () {
680      window.makeClapLoader = function (opts) {
681        opts = opts || {};
682        const tpl = document.getElementById('ss-clap-tpl');
683        const box = document.createElement('div');
684        box.className = 'clap-loader-box' + (opts.inline ? ' inline' : '');
685        if (tpl && tpl.content.firstElementChild) {
686          const svg = tpl.content.firstElementChild.cloneNode(true);
687          const size = opts.size || 48;
688          svg.style.width = size + 'px';
689          svg.style.height = size + 'px';
690          box.appendChild(svg);
691        }
692        if (opts.label) {
693          const l = document.createElement('div');
694          l.className = 'clap-loader-label';
695          l.textContent = opts.label;
696          box.appendChild(l);
697        }
698        return box;
699      };
700      window.clapLoaderHTML = function (opts) {
701        const el = window.makeClapLoader(opts);
702        return el ? el.outerHTML : '';
703      };
704      // Just the clapper SVG (no box/label) for replacing inline <i> spinners inside text.
705      window.clapSvgHTML = function (size) {
706        const tpl = document.getElementById('ss-clap-tpl');
707        if (!tpl || !tpl.content.firstElementChild) return '';
708        const svg = tpl.content.firstElementChild.cloneNode(true);
709        const s = size || 20;
710        svg.style.width = s + 'px'; svg.style.height = s + 'px';
711        svg.style.display = 'inline-block'; svg.style.verticalAlign = 'middle'; svg.style.marginRight = '.45em';
712        return svg.outerHTML;
713      };
714      function hydrate(root) {
715        (root || document).querySelectorAll('[data-clap-loader]:not([data-clap-done])').forEach(function (el) {
716          el.setAttribute('data-clap-done', '1');
717          el.appendChild(window.makeClapLoader({
718            size: parseInt(el.getAttribute('data-size'), 10) || 48,
719            label: el.getAttribute('data-label') || '',
720            inline: el.hasAttribute('data-inline')
721          }));
722        });
723      }
724      window.hydrateClapLoaders = hydrate;
725      if (document.readyState === 'loading') {
726        document.addEventListener('DOMContentLoaded', function () { hydrate(); });
727      } else { hydrate(); }
728    })();
729  </script>
729
730
731  <!-- ========= NAVBAR ========= -->
732  <header class="fixed-top glass-nav">
733    <div class="container">
734      <nav class="navbar navbar-expand-lg navbar-dark" id="navbar_id">
735        <a class="navbar-brand d-flex align-items-center" href="/" aria-label="StreamSnip home">
736          <div class="me-2 site_icon" id="site_icon" style="width:36px;height:36px;flex-shrink:0;overflow:visible;cursor:pointer;">
737            <svg id="_nbl_svg" viewBox="0 0 3334 3334" width="36" height="36" xmlns="http://www.w3.org/2000/svg" style="overflow:visible;display:block;" aria-hidden="true" focusable="false">
738              <style>
739                @keyframes _nbl_clap {
740                  0%   { transform: rotate(-17deg); animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); }
741                  18%  { transform: rotate(-57deg); animation-timing-function: linear; }
742                  38%  { transform: rotate(-57deg); animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
743                  51%  { transform: rotate(0deg);   animation-timing-function: cubic-bezier(0, 0, 0.3, 1); }
744                  58%  { transform: rotate(-8deg);  animation-timing-function: cubic-bezier(0.25, 1, 0.5, 1); }
745                  66%  { transform: rotate(0deg);   animation-timing-function: linear; }
746                  80%  { transform: rotate(0deg);   animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); }
747                  100% { transform: rotate(-17deg); }
748                }
749                #_nbl_stick {
750                  transform-origin: 958px 614px;
751                  transform: rotate(-17deg);
752                }
753                #_nbl_stick._nbl_play {
754                  animation: _nbl_clap 2.6s linear 1 forwards;
755                }
756              </style>
757              <!-- Punch the hinge hole through the icon so the stick can't be seen through it
758                   as it claps. SVG mask (true transparency for the glass navbar); region is set
759                   huge so the swinging stick is never clipped. Circle = the board hole in
760                   user space (cx 640, cy 891, r 112 of the 3334 viewBox). -->
761              <mask id="_nbl_holemask" maskUnits="userSpaceOnUse" x="-2000" y="-2000" width="8000" height="8000">
762                <rect x="-2000" y="-2000" width="8000" height="8000" fill="#fff"/>
763                <circle cx="640" cy="891" r="112" fill="#000"/>
764              </mask>
765              <g mask="url(#_nbl_holemask)">
766              <path id="_nbl_stick" fill-rule="evenodd" paint-order="stroke fill"
767                transform="matrix(0.999958, -0.009206, 0.009206, 0.999958, 0.019399, 0.012944)"
768                d="M 2630.692 408.528 C 2630.622 408.998 2932.538 739.198 2932.538 739.198 L 2935.557 411.334 M 2329.824 405.758 C 2329.564 405.972 1890.172 401.018 1890.292 401.711 C 1890.412 402.336 2203.145 732.254 2205.705 732.506 C 2207.355 732.67 2629.269 736.133 2629.769 735.802 M 1532.807 398.421 C 1532.557 398.615 1083.58 393.355 1083.68 394.286 C 1083.76 395.084 1399.75 724.278 1400.29 724.803 C 1400.82 725.329 1835.934 729.102 1835.931 729.103 M 471.788 388.873 L 468.835 716.516 C 468.835 716.516 1089.13 725.939 1089.05 724.765 C 1088.98 723.601 779.835 391.763 776.285 391.456"/>
769              <path id="_nbl_clapper" fill-rule="evenodd" paint-order="stroke fill"
770                transform="matrix(0.95517403, -0.29604402, 0.29604402, 0.95517403, -456.95786825, 588.0155987)"
771                d="M 524.433 1432.53 C 365.242 1947.64 234.251 2372.63 233.351 2376.95 C 232.451 2381.27 231.481 2389.75 231.191 2395.8 C 230.841 2403.26 231.181 2410.12 232.261 2417.13 C 233.131 2422.81 235.451 2432.26 237.411 2438.13 C 239.381 2444 242.971 2452.44 245.401 2456.9 C 247.831 2461.35 251.911 2467.88 254.481 2471.4 C 257.041 2474.92 262.871 2481.46 267.431 2485.94 C 271.991 2490.42 279.241 2496.41 283.551 2499.25 C 287.851 2502.1 295.001 2506.17 299.451 2508.31 C 305.091 2511.02 623.563 2609.86 1356.05 2836.23 C 1932.73 3014.45 2408.35 3161.15 2412.97 3162.23 C 2417.59 3163.32 2425.42 3164.74 2430.37 3165.38 C 2436.04 3166.13 2445.47 3166.35 2455.87 3165.98 C 2466.63 3165.6 2475.74 3164.68 2482.05 3163.34 C 2487.37 3162.21 2495.92 3159.92 2501.05 3158.25 C 2506.18 3156.57 2515.08 3152.96 2520.84 3150.21 C 2526.6 3147.46 2535.6 3142.35 2540.85 3138.86 C 2546.09 3135.37 2554.2 3129.17 2558.87 3125.08 C 2563.55 3121 2570.58 3113.86 2574.51 3109.23 C 2578.43 3104.59 2584.16 3097.03 2587.23 3092.43 C 2590.31 3087.83 2595.04 3079.5 2597.75 3073.93 C 2600.47 3068.36 2604.3 3058.85 2606.28 3052.8 
771C 2608.26 3046.75 2741.7 2615.21 2902.81 2093.83 C 3063.93 1572.44 3198.395 1137.842 3198.215 1137.672 C 3198.045 1137.492 2817.49 1343.35 2817.49 1343.35 C 2817.49 1343.35 2528.38 1254.14 2528.24 1254 C 2528.1 1253.86 2909.86 1047.81 2908.62 1047.27 C 2907.38 1046.73 2503.85 922.031 2503.22 921.811 C 2502.59 921.601 2109.55 1124.54 2109.55 1124.54 C 2109.55 1124.54 1767.13 1018.87 1765.62 1018.58 C 1764.11 1018.3 2151.16 813.221 2150.17 812.831 C 2149.18 812.441 1730.94 683.301 1730.12 683.301 C 1729.31 683.301 1363.89 871.901 1354.74 876.651 C 1345.59 881.401 1337.18 885.291 1336.04 885.291 C 1334.9 885.291 1062.03 800.871 1055.5 798.831 C 1048.97 796.791 1044.03 794.771 1044.5 794.341 C 1044.98 793.921 1433.417 591.777 1433.197 590.627 C 1432.977 589.477 881.045 419.444 844.455 408.154 M 943.532 525.271 C 940.872 525.761 935.492 527.121 931.582 528.311 C 927.672 529.501 921.022 532.201 916.812 534.321 C 912.602 536.441 905.722 540.901 901.522 544.241 C 897.322 547.581 891.112 553.791 887.712 558.051 C 884.182 562.481 879.512 570.071 876.812 575.791 C 874.201 581.291 871.321 588.721 870.391 592.291 C 869.471 595.871 868.471 603.971 868.181 610.291 C 867.781 618.801 868.071 624.131 869.331 630.771 C 870.261 635.701 872.341 643.171 873.961 647.361 C 875.572 651.551 879.542 658.981 882.772 663.891 C 886.012 668.781 891.532 675.561 895.052 678.931 C 898.582 682.311 904.952 687.341 909.212 690.131 C 913.472 692.911 920.422 696.521 924.662 698.151 C 928.902 699.781 935.842 701.831 940.082 702.711 C 944.322 703.581 952.322 704.301 957.872 704.301 C 963.422 704.301 971.422 703.581 975.662 702.711 C 979.902 701.831 986.842 699.781 991.082 698.151 C 995.322 696.521 1002.27 692.911 1006.53 690.131 C 1010.79 687.341 1017.16 682.311 1020.67 678.931 C 1024.19 675.561 1029.48 669.171 1032.43 664.741 C 1035.38 660.311 1039.22 653.341 104
7710.96 649.241 C 1042.7 645.151 1044.96 637.741 1045.99 632.791 C 1047.01 627.841 1047.85 619.971 1047.85 615.291 C 1047.85 610.621 1047.39 603.871 1046.83 600.291 C 1046.26 596.721 1044.86 590.871 1043.7 587.291 C 1042.54 583.721 1039.88 577.371 1037.79 573.181 C 1035.7 569.001 1031.26 562.141 1027.92 557.941 C 1024.59 553.741 1018.58 547.701 1014.58 544.521 C 1010.58 541.341 1003.7 536.861 999.292 534.571 C 994.882 532.281 987.242 529.211 982.322 527.761 C 975.302 525.691 970.682 525.041 960.872 524.761 C 953.992 524.561 946.192 524.791 943.532 525.271 M 1097.12 1239.09 C 964.982 1320.88 856.881 1388.02 856.881 1388.3 C 856.891 1388.57 1003.82 1573.48 1183.41 1799.2 L 1509.93 2209.59 L 1427.65 2264.44 L 1345.37 2319.28 L 1152.07 2252.4 C 1045.75 2215.62 958.492 2185.25 958.152 2184.91 C 957.812 2184.57 957.722 2184.15 957.952 2183.96 C 958.182 2183.78 1016.2 2150.17 1086.87 2109.29 C 1157.55 2068.4 1216.6 2034.2 1218.11 2033.29 L 1220.84 2031.64 L 1145.29 1932.72 C 1103.73 1878.31 1069.31 1833.91 1068.79 1834.05 C 1068.26 1834.2 914.962 1929.76 728.101 2046.42 C 541.253 2163.08 387.822 2259.04 387.162 2259.66 C 386.242 2260.52 508.042 2300.95 886.112 2425.3 C 1161.21 2515.77 1386.88 2589.8 1387.61 2589.8 C 1388.34 2589.8 1495.56 2520.05 1625.87 2434.8 C 1759.06 2347.67 1862.63 2279.29 1862.4 2278.63 C 1862.18 2277.99 1715.65 2093.49 1536.79 1868.63 C 1357.93 1643.77 1211.2 1459.31 1210.74 1458.71 C 1210.18 1457.99 1237.69 1439.08 1292.07 1402.83 L 1374.26 1348.04 L 1468.67 1372.7 L 1563.09 1397.36 L 1687.73 1320.88 C 1756.28 1278.81 1813.17 1243.81 1814.16 1243.08 C 1815.7 1241.95 1784.17 1231.67 1577.66 1166 C 1446.6 1124.32 1338.92 1090.25 1338.37 1090.29 C 1337.82 1090.33 1229.26 1157.29 1097.12 1239.09 M 1717.29 1441.36 C 1586.98 1526.65 1480.26 1596.74 1480.14 1597.11 C 1480.01 1597.49 1626.84 1782.47 1806.43 2008.19 L 2132.96 2418.58 L 2050.66 2473.42 L 1968.37 2528.27 L 1874.5 2503.73 C 1822.87 2490.24 1780.1 2479.33 1779.45 2479.5 C 1778.8 2479.66 1721.93 2514.38 1653.07 2556.65 C 1584.21 2598.92 1527.87 2633.8 1527.87 2634.17 C 1527.87 2634.54 1635.09 2668.96 1766.14 2710.66 L 2004.42 2786.47 L 2245.1 2637.64 C 2388.34 2549.07 2485.63 2488.31 2485.38 2487.59 C 2485.15 2486.93 2338.63 2302.43 2159.78 2077.59 C 1980.93 1852.76 1834.22 1668.31 1833.75 1667.71 C 1833.18 1666.98 1860.5 1648.21 1915.06 1611.84 L 1997.23 1557.05 L 2012.3 1562.22 C 2020.59 1565.06 2107.62 1595.15 2205.7 1629.09 C 2303.78 1663.03 2384.03 1691.25 2384.02 1691.8 C 2384.02 1692.35 2325.36 1726.77 2253.66 1768.3 C 2181.96 1809.82 2123.09 1844.11 2122.84 1844.5 C 2122.58 1844.88 2156.34 1889.71 2197.87 1944.11 L 2273.37 2043.01 L 2279.37 2039.25 C 2282.67 2037.17 2436.12 1941.35 2620.37 1826.31 C 2804.62 1711.26 2955.75 1616.77 2956.21 1616.33 C 2956.67 1615.89 2952.84 1614.14 2947.71 1612.45 C 2942.57 1610.76 2717.3 1536.69 2447.11 1447.84 C 2176.92 1358.99 1955.48 1286.3 1955.03 1286.3 C 1954.57 1286.3 1847.59 1356.08 1717.29 1441.36"/>
772              </g>
773            </svg>
774          </div>
775          <span class="fw-bold tracking-tight d-none d-sm-inline">StreamSnip</span>
776        </a>
777        <button class="navbar-toggler border-0 shadow-none d-lg-none d-flex align-items-center justify-content-center"
778          type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
779          aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"
780          style="width: 44px; height: 44px; padding: 0;">
781          <div class="hamburger-icon">
782            <span></span>
783            <span></span>
784            <span></span>
785          </div>
786        </button>
787
788        <div class="collapse navbar-collapse justify-content-end" id="navbarSupportedContent">
789          <ul class="navbar-nav align-items-lg-center gap-1">
790            <li class="nav-item">
791              <a class="nav-link px-3 highlight-link d-flex align-items-center" href="/snips" title="View Clips Feed">
792                <i class="fa-solid fa-play me-2" aria-hidden="true"></i> Snips
793              </a>
794            </li>
795
796            <li class="nav-item">
797              <a class="nav-link px-3 highlight-link d-flex align-items-center" href="/get-started"
798                title="How to use StreamSnip">
799                <i class="fa-solid fa-rocket me-2 d-lg-none" aria-hidden="true"></i> Get Started
800              </a>
801            </li>
802
803            <li class="nav-item dropdown">
804              <a class="nav-link dropdown-toggle d-flex align-items-center" href="#" id="resourcesDropdown"
805                role="button" data-bs-toggle="dropdown" aria-expanded="false" title="More Resources">
806                <i class="fa-solid fa-folder-open me-2 d-lg-none" aria-hidden="true"></i> Resources
807              </a>
808              <ul class="dropdown-menu dropdown-menu-end glass" aria-labelledby="resourcesDropdown">
809                <li><a class="dropdown-item" href="/settings#discord"><i class="fa-solid fa-link"
810                      aria-hidden="true"></i> Connect
811                    Discord</a></li>
812                <li><a class="dropdown-item"
813                    href="https://chromewebstore.google.com/detail/stream
813snip/jpnlipcmklpfniaheflfnkomjdkhiani"><i
814                      class="fa-solid fa-puzzle-piece" aria-hidden="true"></i> Chrome Extension</a></li>
815                <li><a class="dropdown-item" href="/stats"><i class="fa-solid fa-chart-line" aria-hidden="true"></i>
816                    Dashboard Stats</a>
817                </li>
818                <li><a class="dropdown-item" href="/all_games"><i class="fa-solid fa-gamepad" aria-hidden="true"></i>
819                    All Games</a></li>
820                <li><a class="dropdown-item" href="/guides"><i class="fa-solid fa-book" aria-hidden="true"></i>
821                    Guides</a></li>
822                <li><a class="dropdown-item" href="/ytdlp-builder"><i class="fa-solid fa-terminal" aria-hidden="true"></i>
823                    yt-dlp Builder</a></li>
824                <li><a class="dropdown-item" href="/kick_downloader"><img src="/static/kick-icon.png" alt="" style="height:1em;width:auto;vertical-align:-0.15em;"> Kick VOD Downloader</a></li>
825                <li><a class="dropdown-item" href="/valorant-pov"><i class="fa-solid fa-crosshairs" aria-hidden="true"></i> Valorant POV Finder</a></li>
826                <li><a class="dropdown-item" href="/overlays"><i class="fa-solid fa-layer-group" aria-hidden="true"></i> Stream Overlays</a></li>
827                <li><a class="dropdown-item" href="https://streamsnip.cronitorstatus.com" target="_blank"><i
828                      class="fa-solid fa-signal" aria-hidden="true"></i> System Status</a></li>
829              </ul>
830
831            </li>
832
833            <li class="nav-item">
834              <a class="nav-link px-3 d-flex align-items-center" href="#" id="beamer-whats-new" title="What's New">
835                <i class="fa-solid fa-bell me-2 d-lg-none" aria-hidden="true"></i> <span>What's New</span>
836              </a>
837            </li>
838
839            <li class="nav-item">
840              <a class="nav-link px-3 d-flex align-items-center" href="https://discord.gg/2XVBWK99Vy"
841                title="Join our Discord Community" aria-label="Discord Community">
842                <i class="fa-brands fa-discord me-2" aria-hidden="true"></i> <span class="d-lg-none">Discord</span>
843              </a>
844            </li>
845
846            <li class="nav-item">
847              <a class="nav-link px-3 d-flex align-items-center" href="#" id="report-bug-btn" role="button"
848                title="Report a bug" aria-label="Report a bug" onclick="return false;">
849                <i class="fa-solid fa-bug me-2" aria-hidden="true"></i> <span class="d-lg-none">Report a bug</span>
850              </a>
851            </li>
852
853            
854
855            <li class="nav-item">
856              <a class="nav-link px-3 d-flex align-items-center" href="#" id="theme_button" onclick="toggleDarkTheme()"
857                title="Toggle Light/Dark Theme" aria-label="Toggle Theme">
858                <i class="fa-solid fa-moon fa-fw me-2" aria-hidden="true"></i> <span class="d-lg-none">Toggle Theme</span>
859              </a>
860            </li>
861
862            
863            <li class="nav-item d-lg-none">
864              <a class="nav-link px-3 d-flex align-items-center" href="/login?next=/login">
865                <i class="fa-solid fa-sign-in me-2" aria-hidden="true"></i> Login
866              </a>
867            </li>
868            <li class="nav-item ms-lg-2 d-none d-lg-flex align-items-center">
869              <a class="btn btn-primary btn-sm rounded-pill px-4" href="/login?next=/login">Login <i
870                  class="fa-solid fa-sign-in ms-1" aria-hidden="true"></i></a>
871            </li>
872            
873          </ul>
874        </div>
875
876      </nav>
877      <style>
878        /* Modern Hamburger Menu */
879        .hamburger-icon {
880          width: 22px;
881          height: 16px;
882          position: relative;
883          display: flex;
884          flex-direction: column;
885          justify-content: space-between;
886        }
887
888        .hamburger-icon span {
889          display: block;
890          height: 2px;
891          width: 100%;
892          background-color: var(--color-text);
893          border-radius: 4px;
894          transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease;
895          transform-origin: center center;
896        }
897
898        .navbar-toggler[aria-expanded="true"] .hamburger-icon span:nth-child(1) {
899          transform: translateY(7px) rotate(45deg);
900        }
901
902        .navbar-toggler[aria-expanded="true"] .hamburger-icon span:nth-child(2) {
903          opacity: 0;
904          transform: scale(0);
905        }
906
907        .navbar-toggler[aria-expanded="true"] .hamburger-icon span:nth-child(3) {
908          transform: translateY(-7px) rotate(-45deg);
909        }
910
911        /* Mobile Navbar Fix */
912        @media (max-width: 991px) {
913          .navbar-nav {
914            align-items: flex-start !important;
915            padding-top: 1rem;
916          }
917
918          .navbar-nav .nav-item {
919            width: 100%;
920          }
921
922          .navbar-nav .nav-link {
923            width: 100%;
924            padding: 0.75rem 1rem !important;
925          }
926
927          .navbar-collapse {
928            background: var(--glass-bg) !important;
929            backdrop-filter: blur(24px);
930            -webkit-backdrop-filter: blur(24px);
931            border-radius: var(--radius-lg);
932            margin-top: 0.75rem;
933            padding: 0.5rem;
934            border: 1px solid var(--glass-border);
935            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
936          }
937
938          body .navbar-nav .dropdown-menu {
939            background: var(--glass-bg) !important;
940            background-color: var(--glass-bg) !important;
941            backdrop-filter: blur(12px) !important;
942            -webkit-backdrop-filter: blur(12px) !important;
943            border: 1px solid var(--glass-border) !important;
944            border-inline-start: 3px solid var(--color-primary) !important;
945            margin-block: 0.5rem !important;
946            margin-inline: 1rem 0 !important;
947            padding: 0.5rem !important;
948            box-shadow: none !important;
949            position: static !important;
950            float: none !important;
951            width: calc(100% - 1rem) !important;
952            min-width: 0 !important;
953            transform: none !important;
954            opacity: 0 !important;
955            visibility: hidden !important;
956            display: none !important;
957          }
958
959          body .navbar-nav .dropdown-menu.show {
960            display: block !important;
961            opacity: 1 !important;
962            visibility: visible !important;
963          }
964
965          .dropdown-item {
966            padding: 0.5rem 0.75rem !important;
967            border-radius: var(--radius-sm);
968            font-size: 0.9rem;
969          }
970        }
971
972        /* Beamer panel uses the nav link as its trigger */
973        #beamerSelector {
974          display: none !important;
975          border: 1px solid var(--color-primary);
976        }
977      </style>
978  </header>
979
980  <!-- ========= ALERT CONTAINER ========= -->
981  <style>
982    #custom-alerts-container {
983      position: fixed;
984      bottom: 1.5rem;
985      right: 1.5rem;
986      z-index: 99999;
987      display: flex;
988      flex-direction: column;
989      align-items: flex-end;
990      gap: 0.5rem;
991      pointer-events: none;
992      isolation: isolate;
993    }
994
995    @media (max-width: 767.98px) {
996      #custom-alerts-container {
997        right: 50%;
998        transform: translateX(50%);
999        align-items: center;
1000        width: 100%;
1001        padding: 0 1rem;
1002      }
1003    }
1004
1005    .custom-toast {
1006      pointer-events: auto;
1007      min-width: 250px;
1008      max-width: 100%;
1009      background: rgba(255, 255, 255, 0.9);
1010      backdrop-filter: blur(12px);
1011      -webkit-backdrop-filter: blur(12px);
1012      border: 1px solid rgba(255, 255, 255, 0.5);
1013      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
1014      border-radius: var(--radius-btn);
1015      padding: 1rem 1.25rem;
1016      position: relative;
1017      overflow: visible;
1018      display: flex;
1019      align-items: center;
1020      gap: 0.75rem;
1021      transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1), transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
1022    }
1023
1024    .dark-theme .custom-toast {
1025      background: hsl(var(--accent-h) 26% 12% / 0.85);
1026      border: 1px solid rgba(255, 255, 255, 0.1);
1027      color: var(--color-text);
1028    }
1029
1030    .custom-toast.toast-danger {
1031      border-inline-start: 4px solid var(--color-danger);
1032    }
1033
1034    .custom-toast.toast-success {
1035      border-inline-start: 4px solid var(--color-success);
1036    }
1037
1038    .custom-toast.toast-warning {
1039      border-inline-start: 4px solid var(--color-warning);
1040    }
1041
1042    .custom-toast.toast-info {
1043      border-inline-start: 4px solid var(--color-info);
1044    }
1045
1046    .custom-toast .toast-icon {
1047      font-size: 1.25rem;
1048      flex-shrink: 0;
1049    }
1050
1051    .custom-toast.toast-danger .toast-icon {
1052      color: var(--color-danger);
1053    }
1054
1055    .custom-toast.toast-success .toast-icon {
1056      color: var(--color-success);
1057    }
1058
1059    .custom-toast.toast-warning .toast-icon {
1060      color: var(--color-warning);
1061    }
1062
1063    .custom-toast.toast-info .toast-icon {
1064      color: var(--color-info);
1065    }
1066
1067    .custom-toast .toast-message {
1068      flex-grow: 1;
1069      word-break: break-word;
1070      font-size: 0.95rem;
1071      line-height: 1.4;
1072    }
1073
1074    .custom-toast .toast-action {
1075      flex-shrink: 0;
1076      border: 1px solid var(--glass-border);
1077      background: transparent;
1078      color: inherit;
1079      font: inherit;
1080      font-size: 0.85rem;
1081      font-weight: 600;
1082      padding: 0.35rem 0.75rem;
1083      border-radius: var(--radius-sm, 0.375rem);
1084      cursor: pointer;
1085      transition: background-color var(--transition-fast);
1086    }
1087
1088    .custom-toast .toast-action:hover {
1089      background: var(--overlay-hover, rgba(127, 127, 127, 0.14));
1090    }
1091  </style>
1092  <div id="custom-alerts-container" role="status" aria-live="polite" aria-atomic="false"></div>
1093
1094  
1095
1096  <!-- ========= MAIN CONTENT ========= -->
1097  <main class="content">
1098<div class="login-page">
1099    <div class="login-hero-bg">
1100        <canvas id="login-canvas" aria-hidden="true"></canvas>
1101    </div>
1102
1103    <div class="login-wrapper">
1104        <div class="login-card glass">
1105            <div class="login-header">
1106                <h1>Welcome Back</h1>
1107            </div>
1108
1109            <form action="/login?next=/" method="post">
1110                <div class="input-group-custom">
1111                    <i class="fas fa-link" aria-hidden="true"></i>
1112                    <!-- Placeholder-only fields are invisible to a screen reader once typing
1113                         starts, and this one is the whole login. -->
1114                    <label for="password" class="visually-hidden">Webhook URL or password</label>
1115                    <input type="password" name="password" class="glass-input" placeholder="Webhook URL / Password"
1116                           id="password" required autocomplete="current-password"
1117                           aria-describedby="password-hint">
1118                </div>
1119
1120                <div class="remember-row">
1121                    <label class="remember-label">
1122                        <input type="checkbox" id="remember" name="remember" value="remember">
1123                        Remember this session
1124                    </label>
1125                </div>
1126
1127                <button type="submit" class="btn btn-primary w-100 py-3" style="font-weight: 600; border-radius: var(--radius-md);">
1128                    Sign In <i class="fas fa-arrow-right ms-2" aria-hidden="true"></i>
1129                </button>
1130
1131                <p class="msg-hint" id="password-hint">
1132                    <i class="fas fa-info-circle me-1" aria-hidden="true"></i>
1133                    Your password is the webhook URL registered to your channel.
1134                </p>
1135            </form>
1136
1137            <div class="divider">or</div>
1138
1139            <a href="/login/google?next=/" class="google-btn">
1140                <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
1141                    <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/>
1142                    <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
1143                    <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
1144                    <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
1145                </svg>
1146                Continue with Google
1147            </a>
1148
1149            <div class="signup-hint">
1150                New to StreamSnip? <a href="/login/google?next=/">Create an account</a>
1151            </div>
1152        </div>
1153    </div>
1154</div>
1155</main>
1156
1157  <!-- ========= FOOTER ========= -->
1158  <footer class="py-3 border-top border-opacity-10" style="position: relative; z-index: 2;">
1159
1160    <div class="container">
1161      <div class="row align-items-center">
1162        <div class="col-md-6">© 2026&nbsp;Suraj&nbsp;Bhari</div>
1163        <nav class="col-md-6 text-md-end">
1164          <a href="/privacy-policy">Privacy&nbsp;Policy</a> |
1165          <a href="/aboutus">About&nbsp;Us</a> |
1166          <a href="/contactus">Contact&nbsp;Us</a> |
1167          <a href="/tnc">Terms&nbsp;&amp;&nbsp;Conditions</a>
1168        </nav>
1169      </div>
1170      <div class="row mt-2" aria-hidden="true">
1171        <div class="col-12 text-md-end small" style="color: transparent; user-select: none;">
1172          StreamSnip is original, copyrighted work. Use of any content, code, or design from this site
1173          for AI/ML training, RAG indexing, generative reproduction, or to "rebuild" or "clone" the site
1174          is not permitted.
1175        </div>
1176      </div>
1177    </div>
1178  </footer>
1179
1180  <!-- ========= DOWNLOAD QUEUE ========= -->
1181<style>
1182  #dq-panel { font-family: inherit; }
1183  .dq-row {
1184    border-radius: var(--radius-md);
1185    padding: 0.4rem 0.6rem;
1186    position: relative;
1187    overflow: hidden;
1188    flex-shrink: 0;          /* don't compress — let the body scroll instead */
1189    transition: background 0.2s ease;
1190  }
1191  .dq-row-pending  { background: rgba(255,255,255,0.04); }
1192  .dq-row-downloading { background: var(--primary-08); border: 1px solid hsl(var(--accent-h) var(--accent-s) var(--accent-l) / 0.2); }
1193  .dq-row-done     { background: rgba(34,197,94,0.08);  border: 1px solid rgba(34,197,94,0.2); }
1194  .dq-row-error    { background: rgba(239,68,68,0.08);  border: 1px solid rgba(239,68,68,0.2); }
1195  .dq-label {
1196    font-size: 0.75rem;
1197    color: var(--color-text);
1198    white-space: nowrap;
1199    overflow: hidden;
1200    text-overflow: ellipsis;
1201    max-width: 100%;
1202  }
1203  .dq-sub    { font-size: 0.65rem;
1203 margin-top: 1px; }
1204  .dq-detail { font-size: 0.6rem; margin-top: 1px; color: var(--color-text-muted); opacity: 0.7; letter-spacing: 0.02em; }
1205  .dq-actions {
1206    display: flex;
1207    align-items: center;
1208    justify-content: space-between;
1209    gap: 4px;
1210    margin-top: 3px;
1211  }
1212  .dq-btn {
1213    font-size: 0.65rem;
1214    padding: 2px 6px;
1215    border-radius: 4px;
1216    border: none;
1217    cursor: pointer;
1218    line-height: 1.4;
1219    text-decoration: none;
1220    display: inline-flex;
1221    align-items: center;
1222    gap: 3px;
1223  }
1224  .dq-btn-dl   { background: rgba(34,197,94,0.2);  color: #86efac; }
1225  .dq-btn-dl:hover { background: rgba(34,197,94,0.35); color: #86efac; }
1226  .dq-btn-retry { background: rgba(239,68,68,0.2); color: #fca5a5; }
1227  .dq-btn-retry:hover { background: rgba(239,68,68,0.35); color: #fca5a5; }
1228  .dq-btn-rm   { background: none; color: var(--color-text-muted); opacity: 0.6; }
1229  .dq-btn-rm:hover { opacity: 1; }
1230  #dq-body::-webkit-scrollbar { width: 4px; }
1231  #dq-body::-webkit-scrollbar-thumb { background: var(--color-primary); border-radius: 4px; }
1232  /* downloading row indicator */
1233  .bouncing-gradient {
1234    background: var(--primary-12);
1235    border-inline-start: 3px solid var(--color-primary) !important;
1236  }
1237</style>
1238
1239<div id="dq-panel" style="display:none;position:fixed;bottom:1.5rem;inset-inline-start:1.5rem;z-index:1030;width:260px;">
1240  <div class="glass" style="border-radius:var(--radius-lg);overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,0.3);">
1241    <!-- Header -->
1242    <div id="dq-header"
1243         onclick="DownloadQueue._toggleCollapse()"
1244         style="display:flex;align-items:center;justify-content:space-between;padding:0.55rem 0.75rem;background:var(--glass-bg-subtle);border-bottom:1px solid var(--glass-border);cursor:pointer;user-select:none;">
1245      <span style="font-size:0.68rem;font-weight:700;letter-spacing:0.05em;color:var(--color-text-muted);text-transform:uppercase;display:flex;align-items:center;gap:6px;">
1246        <i class="fa-solid fa-download" style="color:var(--color-primary);font-size:0.7rem;" aria-hidden="true"></i>
1247        <span id="dq-title">Downloads</span>
1248      </span>
1249      <div style="display:flex;align-items:center;gap:6px;">
1250        <a href="/downloads"
1251           onclick="event.stopPropagation();"
1252           style="font-size:0.6rem;color:var(--color-text-muted);padding:1px 4px;opacity:0.65;line-height:1;text-decoration:none;"
1253           title="Open the downloads page">view all</a>
1254        <button id="dq-clear-btn"
1255                onclick="event.stopPropagation();DownloadQueue.clearAll()"
1256                style="display:none;background:none;border:none;cursor:pointer;font-size:0.6rem;color:var(--color-text-muted);padding:1px 4px;opacity:0.65;line-height:1;"
1257                title="Cancel all queued items">clear all</button>
1258        <i id="dq-chevron" class="fa-solid fa-chevron-down" style="font-size:0.6rem;color:var(--color-text-muted);transition:transform 0.2s ease;" aria-hidden="true"></i>
1259      </div>
1260    </div>
1261    <!-- Item list -->
1262    <div id="dq-body"
1263         style="padding:0.4rem;display:flex;flex-direction:column;gap:0.3rem;max-height:180px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--color-primary) transparent;">
1264    </div>
1265  </div>
1266</div>
1267
1268<script>
1269/* =================== TAB BADGE =================== */
1270/* One owner of document.title, because two of them fight: whichever captures the
1271   base title second captures the other's "(2) " prefix and bakes it in for good.
1272   Anything that finishes while the tab is hidden counts in here; coming back to
1273   the tab is what clears it. */
1274window.TabBadge = (() => {
1275  let base  = null;
1276  let count = 0;
1277
1278  function _base() {
1279    if (base === null) base = document.title.replace(/^\(\d+\)\s+/, '');
1280    return base;
1281  }
1282
1283  function set(n) {
1284    count = Math.max(0, n | 0);
1285    document.title = count ? `(${count}) ${_base()}` : _base();
1286  }
1287
1288  function bump(n) { set(count + (n || 1)); }
1289  function clear()  { set(0); }
1290
1291  document.addEventListener('visibilitychange', () => {
1292    if (!document.hidden) clear();
1293  });
1294
1295  return { set, bump, clear, get count() { return count; } };
1296})();
1297
1298/* =================== SHARE =================== */
1299/* On a phone, copying a link to the clipboard is the long way round to the thing
1300   people actually want — the OS share sheet, one tap to WhatsApp or a Discord DM.
1301   On desktop the sheet is a worse clipboard (Windows routes it through a half-empty
1302   apps list), so desktop keeps copying. Coarse pointer AND a phone-width screen:
1303   a touchscreen laptop is not a phone, and neither is a tablet in landscape.
1304   Call this from a click handler — navigator.share requires a user gesture. */
1305window.shareOrCopy = function (url, opts) {
1306  opts = opts || {};
1307  const onCopied = opts.onCopied || function () { send_alert('Link copied to clipboard!', 'success'); };
1308  const onFailed = opts.onFailed || function () { send_alert('Failed to copy link.', 'danger'); };
1309  const isPhone = window.matchMedia('(pointer: coarse) and (max-width: 820px)').matches;
1310
1311  if (isPhone && navigator.share) {
1312    const payload = { url: url };
1313    if (opts.title) payload.title = opts.title;
1314    // A dismissed sheet rejects with AbortError — that's a decision, not a failure.
1315    return navigator.share(payload).catch(function (err) {
1316      if (err && err.name === 'AbortError') return;
1317      navigator.clipboard.writeText(url).then(onCopied).catch(onFailed);
1318    });
1319  }
1320  return navigator.clipboard.writeText(url).then(onCopied).catch(onFailed);
1321};
1322
1323/* =================== DOWNLOAD QUEUE =================== */
1324window.DownloadQueue = (() => {
1325  const STORAGE_KEY  = 'ss_download_queue';
1326  const COLLAPSE_KEY = 'ss_dq_collapsed';
1327  const NOTIFY_KEY   = 'ss_dq_downloads_started';
1328
1329  /* A cut can take minutes and people tab away; the badge in the corner only
1330     helps someone already looking at it. Ask for notification permission on the
1331     second download rather than the first — one download is not yet a habit, and
1332     a prompt on the very first click reads as a shakedown (Chrome also holds a
1333     denial against the origin for good). The ask rides the click that triggered
1334     it, so Firefox's user-gesture requirement is satisfied. */
1335  function _maybeAskNotify() {
1336    if (!('Notification' in window) || Notification.permission !== 'default') return;
1337    let started = 0;
1338    try { started = parseInt(localStorage.getItem(NOTIFY_KEY) || '0', 10) || 0; } catch { return; }
1339    started++;
1340    try { localStorage.setItem(NOTIFY_KEY, String(started)); } catch {}
1341    if (started < 2) return;
1342    try { Notification.requestPermission(); } catch {}
1343  }
1344
1345  function _notify(item) {
1346    if (!document.hidden) return;
1347    TabBadge.bump();
1348    if (!('Notification' in window) || Notification.permission !== 'granted') return;
1349    try {
1350      const n = new Notification(
1351        item.status === 'done' ? 'Your clip is ready' : 'Clip download failed',
1352        {
1353          body: item.label || '',
1354          icon: '/static/logo.png',
1355          // Same clip re-firing replaces its own notification instead of stacking.
1356          tag:  'ss-dq-' + item.id,
1357        }
1358      );
1359      n.onclick = () => { window.focus(); n.close(); };
1360    } catch {}
1361  }
1362
1363  function _load() {
1364    try {
1365      const raw = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
1366      const SIX_HOURS = 6 * 60 * 60 * 1000;
1367      const now = Date.now();
1368      const fresh = raw.filter(item => {
1369        if ((item.status === 'done' || item.status === 'error') && item.doneAt) {
1370          return now - item.doneAt < SIX_HOURS;
1371        }
1372        return true;
1373      });
1374      if (fresh.length !== raw.length) localStorage.setItem(STORAGE_KEY, JSON.stringify(fresh));
1375      return fresh;
1376    } catch { return []; }
1377  }
1378
1379  function _save(queue) {
1380    localStorage.setItem(STORAGE_KEY, JSON.stringify(queue));
1381  }
1382
1383  function _esc(s) {
1384    return String(s)
1385      .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
1386  }
1387
1388  function _isCollapsed() {
1389    return localStorage.getItem(COLLAPSE_KEY) === 'true';
1390  }
1391
1392  function _toggleCollapse() {
1393    const next = !_isCollapsed();
1394    localStorage.setItem(COLLAPSE_KEY, next);
1395    _applyCollapse(next);
1396  }
1397
1398  function _applyCollapse(collapsed) {
1399    const body    = document.getElementById('dq-body');
1400    const chevron = document.getElementById('dq-chevron');
1401    if (body)    body.style.display      = collapsed ? 'none'  : 'flex';
1402    if (chevron) chevron.style.transform = collapsed ? 'rotate(-90deg)' : '';
1403  }
1404
1405  function _formatDetail(item) {
1406    const parts = [];
1407    // Use stored human-readable label when available (e.g. "720p · MP4")
1408    if (item.detail) {
1409      parts.push(item.detail);
1410    } else {
1411      // Fallback for items added before detail was stored
1412      try {
1413        const p   = new URLSearchParams(item.qs);
1414        const fid = p.get('format_id') || '';
1415        const ext = (p.get('ext') || '').toUpperCase();
1416        if (fid.startsWith('custom-')) parts.push(fid.replace('custom-', '') + 'p');
1417        else if (fid) parts.push(fid);
1418        if (ext) parts.push(ext);
1419      } catch {}
1420    }
1421    // Always append clip duration when start/end are set
1422    try {
1423      const p  = new URLSearchParams(item.qs);
1424      const t1 = parseInt(p.get('start_time')), t2 = parseInt(p.get('end_time'));
1425      if (!isNaN(t1) && !isNaN(t2) && t2 > t1) {
1426        const dur = t2 - t1;
1427        const mm  = String(Math.floor(dur / 60)).padStart(2, '0');
1428        const ss  = String(dur % 60).padStart(2, '0');
1429        parts.push(`${mm}:${ss} clip`);
1430      }
1431    } catch {}
1432    return parts.join(' · ');
1433  }
1434
1435  function _render() {
1436    const queue  = _load();
1437    const panel  = document.getElementById('dq-panel');
1438    const body   = document.getElementById('dq-body');
1439    const title  = document.getElementById('dq-title');
1440    if (!panel || !body) return;
1441
1442    panel.style.display = queue.length === 0 ? 'none' : 'block';
1443    if (title) title.textContent = `Downloads (${queue.length})`;
1444    const clearBtn = document.getElementById('dq-clear-btn');
1445    if (clearBtn) clearBtn.style.display = queue.length > 0 ? 'inline' : 'none';
1446
1447    body.innerHTML = '';
1448    // Sort: downloading first, then pending, then error, then done (done: most recent on top)
1449    const _statusOrder = { downloading: 0, pending: 1, error: 2, done: 3 };
1450    const sorted = queue
1451      .map((item, i) => ({ item, i }))
1452      .sort((a, b) => {
1453        const diff = (_statusOrder[a.item.status] ?? 9) - (_statusOrder[b.item.status] ?? 9);
1454        if (diff !== 0) return diff;
1455        if (a.item.status === 'done') return b.i - a.i; // newest done first
1456        return a.i - b.i;
1457      })
1458      .map(({ item }) => item);
1459    sorted.forEach(item => {
1460      const row = document.createElement('div');
1461      row.className = `dq-row dq-row-${item.status}`;
1462      row.id = `dq-row-${CSS.escape(item.id)}`;
1463
1464      const labelEl = document.createElement('div');
1465      labelEl.className = 'dq-label';
1466      labelEl.title = item.label;
1467
1468      if (item.status === 'pending') {
1469        labelEl.textContent = item.label;
1470        const actions = document.createElement('div');
1471        actions.className = 'dq-actions';
1472
1473        const sub = document.createElement('div');
1474        sub.className = 'dq-sub';
1475        sub.style.color = 'var(--color-text-muted)';
1476        sub.textContent = 'queued';
1477
1478        const rmBtn = document.createElement('button');
1479        rmBtn.className = 'dq-btn dq-btn-rm';
1480        rmBtn.innerHTML = '<i class="fa-solid fa-xmark" aria-hidden="true"></i>';
1481        rmBtn.title = 'Cancel';
1482        rmBtn.setAttribute('aria-label', 'Cancel ' + (item.label || 'this download'));
1483        rmBtn.onclick = () => remove(item.id);
1484
1485        actions.appendChild(sub);
1486        actions.appendChild(rmBtn);
1487        row.appendChild(labelEl);
1488        const det0 = _formatDetail(item);
1489        if (det0) { const d = document.createElement('div'); d.className = 'dq-detail'; d.textContent = det0; row.appendChild(d); }
1490        row.appendChild(actions);
1491
1492      } else if (item.status === 'downloading') {
1493        row.classList.add('bouncing-gradient');
1494        labelEl.textContent = item.label;
1495        const actions = document.createElement('div');
1496        actions.className = 'dq-actions';
1497
1498        const sub = document.createElement('div');
1499        sub.className = 'dq-sub';
1500        sub.style.color = 'var(--color-primary)';
1501        sub.textContent = 'downloading…';
1502
1503        const rmBtn = document.createElement('button');
1504        rmBtn.className = 'dq-btn dq-btn-rm';
1505        rmBtn.innerHTML = '<i class="fa-solid fa-xmark" aria-hidden="true"></i>';
1506        rmBtn.title = 'Cancel';
1507        rmBtn.setAttribute('aria-label', 'Cancel ' + (item.label || 'this download'));
1508        rmBtn.onclick = () => remove(item.id);
1509
1510        actions.appendChild(sub);
1511        actions.appendChild(rmBtn);
1512        row.appendChild(labelEl);
1513        const det1 = _formatDetail(item);
1514        if (det1) { const d = document.createElement('div'); d.className = 'dq-detail'; d.textContent = det1; row.appendChild(d); }
1515        row.appendChild(actions);
1516
1517      } else if (item.status === 'done') {
1518        labelEl.innerHTML = '<i class="fa-solid fa-check me-1" style="color:#4ade80"></i>' + _esc(item.label);
1519        const actions = document.createElement('div');
1520        actions.className = 'dq-actions';
1521
1522        const sub = document.createElement('div');
1523        sub.className = 'dq-sub';
1524        sub.style.color = '#4ade80';
1525        sub.textContent = 'ready';
1526
1527        const dlBtn = document.createElement('a');
1528        dlBtn.href = `/video/${item.id}?${item.qs}`;
1529        dlBtn.className = 'dq-btn dq-btn-dl';
1530        dlBtn.innerHTML = '<i class="fa-solid fa-download" aria-hidden="true"></i>';
1531        dlBtn.title = 'Save file';
1532        dlBtn.setAttribute('aria-label', 'Save ' + (item.label || 'this download'));
1533
1534        const rmBtn = document.createElement('button');
1535        rmBtn.className = 'dq-btn dq-btn-rm';
1536        rmBtn.innerHTML = '<i class="fa-solid fa-xmark" aria-hidden="true"></i>';
1537        rmBtn.title = 'Remove from the list';
1538        rmBtn.setAttribute('aria-label', 'Remove ' + (item.label || 'this download') + ' from the list');
1539        rmBtn.onclick = () => remove(item.id);
1540
1541        actions.appendChild(sub);
1542        actions.appendChild(dlBtn);
1543        actions.appendChild(rmBtn);
1544        row.appendChild(labelEl);
1545        const det2 = _formatDetail(item);
1546        if (det2) { const d = document.createElement('div'); d.className = 'dq-detail'; d.textContent = det2; row.appendChild(d); }
1547        row.appendChild(actions);
1548
1549      } else if (item.status === 'error') {
1550        labelEl.innerHTML = '<i class="fa-solid fa-xmark me-1" style="color:#f87171"></i>' + _esc(item.label);
1551        const actions = document.createElement('div');
1552        actions.className = 'dq-actions';
1553
1554        const sub = document.createElement('div');
1555        sub.className = 'dq-sub';
1556        sub.style.color = '#f87171';
1557        sub.textContent = (item.error || 'Error').substring(0, 38);
1558
1559        const retryBtn = document.createElement('button');
1560        retryBtn.className = 'dq-btn dq-btn-retry';
1561        retryBtn.innerHTML = '<i class="fa-solid fa-rotate-right" aria-hidden="true"></i> retry';
1562        retryBtn.setAttribute('aria-label', 'Retry ' + (item.label || 'this download'));
1563        retryBtn.onclick = () => retry(item.id);
1564
1565        const rmBtn = document.createElement('button');
1566        rmBtn.className = 'dq-btn dq-btn-rm';
1567        rmBtn.innerHTML = '<i class="fa-solid fa-xmark" aria-hidden="true"></i>';
1568        rmBtn.title = 'Remove from the list';
1569        rmBtn.setAttribute('aria-label', 'Remove ' + (item.label || 'this download') + ' from the list');
1570        rmBtn.onclick = () => remove(item.id);
1571
1572        actions.appendChild(sub);
1573        actions.appendChild(retryBtn);
1574        actions.appendChild(rmBtn);
1575        row.appendChild(labelEl);
1576        const det3 = _formatDetail(item);
1577        if (det3) { const d = document.createElement('div'); d.className = 'dq-detail'; d.textContent = det3; row.appendChild(d); }
1578        row.appendChild(actions);
1579      }
1580
1581      body.appendChild(row);
1582    });
1583
1584    _applyCollapse(_isCollapsed());
1585  }
1586
1587  /* ---------- public mutators ---------- */
1588
1589  function add(id, label, qs, detail) {
1590    const queue = _load();
1591    // One row per clip id — _settle/_pollJob/_triggerDownload all key off the
1592    // (assumed-unique) id. Re-download an already-finished clip and we must
1593    // re-activate its existing row, not push a duplicate: a duplicate id makes
1594    // _settle update the wrong row, leaving the new one stuck 'downloading' and
1595    // re-firing the save dialog on every page load.
1596    const existing = queue.find(i => i.id === id);
1597    if (existing) {
1598      if (existing.status === 'pending' || existing.status === 'downloading') return;
1599      existing.status = 'pending';
1600      existing.label  = label;
1601      existing.qs     = qs;
1602      existing.detail = detail || '';
1603      delete existing.error;
1604      delete existing.doneAt;
1605    } else {
1606      queue.push({ id, label, qs, detail: detail || '', status: 'pending' });
1607    }
1608    if (typeof gtag === 'function') gtag('event', 'clip_download', { clip_id: id, source: 'youtube' });
1609    _maybeAskNotify();
1610    _save(queue);
1611    _render();
1612    _kickProcessor();
1613  }
1614
1615  function remove(id) {
1616    _save(_load().filter(i => i.id !== id));
1617    _render();
1618  }
1619
1620  function clearAll() {
1621    _save([]);
1622    _render();
1623  }
1624
1625  function retry(id) {
1626    const queue = _load();
1627    const item  = queue.find(i => i.id === id);
1628    if (item) { item.status = 'pending'; delete item.error; }
1629    _save(queue);
1630    _render();
1631    _kickProcessor();
1632  }
1633
1634  /* ---------- processor ---------- */
1635
1636  let _processing = false;
1637
1638  function _kickProcessor() {
1639    if (!_processing) _process();
1640  }
1641
1642  function _process() {
1643    const queue = _load();
1644    const next  = queue.find(i => i.status === 'pending');
1645    if (!next) { _processing = false; return; }
1646    _processing = true;
1647    next.status = 'downloading';
1648    _save(queue);
1649    _render();
1650    document.dispatchEvent(new CustomEvent('dq:downloading', { detail: { id: next.id, qs: next.qs } }));
1651
1652    fetch(`/download/${next.id}?${next.qs}`, { method: 'POST' })
1653      .then(res => res.json())
1654      .then(({ job_id }) => _pollJob(job_id, next))
1655      .catch(() => _settle(next, 'error', 'Could not start download'))
1656      .finally(() => { _processing = false; _kickProcessor(); });
1657  }
1658
1659  function _pollJob(jobId, item) {
1660    return new Promise(resolve => {
1661      const tick = () => {
1662        // Stop polling if the item was removed from the queue mid-flight,
1663        // so a stuck/long-lived job doesn't leave an orphaned 2s loop running.
1664        if (!_load().some(i => i.id === item.id)) { resolve(); return; }
1665        fetch(`/jobs/${jobId}`)
1666          .then(r => r.json())
1667          .then(d => {
1668            if (d.status === 'succeeded') { _settle(item, 'done'); _triggerDownload(item); resolve(); }
1669            else if (d.status === 'failed' || d.status === 'unknown' || d.status === 'forbidden')
1670                 { _settle(item, 'error', d.error || 'Download failed'); resolve(); }
1671            else setTimeout(tick, 2000); // queued | doing
1672          })
1673          .catch(() => { _settle(item, 'error', 'Lost connection to job'); resolve(); });
1674      };
1675      tick();
1676    });
1677  }
1678
1679  function _settle(next, status, error) {
1680    const q = _load();
1681    const item = q.find(i => i.id === next.id);
1682    if (!item) return;
1683    item.status = status;
1684    if (error) item.error = error;
1685    item.doneAt = Date.now();
1686    _save(q);
1687    _render();
1688    _notify(item);
1689    document.dispatchEvent(new CustomEvent('dq:settled', { detail: { id: item.id, status: item.status } }));
1690  }
1691
1692  function _triggerDownload(item) {
1693    const a = document.createElement('a');
1694    a.href = `/video/${item.id}?${item.qs}`;
1695    a.style.display = 'none';
1696    document.body.appendChild(a);
1697    a.click();
1698    document.body.removeChild(a);
1699  }
1700
1701  /* ---------- init ---------- */
1702
1703  function _init() {
1704    const queue = _load();
1705    // Self-heal duplicate clip-id rows the old add() left in storage — they broke
1706    // _settle's find-by-id and stranded rows on 'downloading' (re-prompting a save
1707    // on every load). Collapse to one row per id (keep the latest), then reset any
1708    // row left 'downloading' by a previous page load back to 'pending'.
1709    const byId = new Map();
1710    queue.forEach(item => byId.set(item.id, item));
1711    const deduped = [...byId.values()];
1712    let changed = deduped.length !== queue.length;
1713    deduped.forEach(item => {
1714      if (item.status === 'downloading') { item.status = 'pending'; changed = true; }
1715    });
1716    if (changed) _save(deduped);
1717    _render();
1718    _kickProcessor();
1719  }
1720
1721  return { add, remove, retry, clearAll, _render, _load, _save, _toggleCollapse, _applyCollapse, _kickProcessor, _process, _init };
1722})();
1723
1724document.addEventListener('DOMContentLoaded', () => DownloadQueue._init());
1725</script>
1725
1726
1727  <!-- ========= SCRIPTS ========= -->
1728  
1728<script src="https://code.jquery.com/jquery-3.7.1.min.js"
1729    integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous" defer></script>
1729
1730
1731  
1731<script
1732    src="https://cdn.jsdelivr.net/gh/xcash/[email protected]/dist/latest/bootstrap-autocomplete.min.js"
1733    defer></script>
1733
1734  
1734<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
1735    integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"
1736    defer></script>
1736
1737  
1737<script src="/static/dropdown.js?v=ccddf8d66c" defer></script>
1737
1738  
1738<script defer>
1739    /* A nav menu's frosted blur is a no-op while it lives inside .glass-nav: the header
1740       is a backdrop root (backdrop-filter + view-transition-name), so a descendant's
1741       backdrop-filter has no page pixels to sample. Lift the menu out to <body> while
1742       it's open and anchor it to the toggle — the header is fixed, so that anchor never
1743       moves and no scroll repositioning is needed. Expanded navbar only; collapsed, the
1744       menu renders inline as an accordion and must stay where it is. */
1745    (function () {
1746      const desktop = window.matchMedia('(min-width: 992px)');
1747      const pairs = [];
1748
1749      function place(menu, toggle) {
1750        const rect = toggle.getBoundingClientRect();
1751        menu.style.setProperty('top', `${rect.bottom + 8}px`, 'important');
1752        menu.style.setProperty('right', `${document.documentElement.clientWidth - rect.right}px`, 'important');
1753      }
1754
1755      for (const toggle of document.querySelectorAll('header.glass-nav [data-bs-toggle="dropdown"]')) {
1756        const menu = toggle.parentElement.querySelector('.dropdown-menu');
1757        if (!menu) continue;
1758        const home = menu.parentElement;
1759        pairs.push([menu, toggle]);
1760
1761        toggle.addEventListener('show.bs.dropdown', () => {
1762          if (!desktop.matches) return;
1763          document.body.appendChild(menu);
1764          menu.classList.add('dropdown-menu-detached');
1765          place(menu, toggle);
1766        });
1767
1768        toggle.addEventListener('hidden.bs.dropdown', () => {
1769          if (!menu.classList.contains('dropdown-menu-detached')) return;
1770          menu.classList.remove('dropdown-menu-detached');
1771          menu.style.removeProperty('top');
1772          menu.style.removeProperty('right');
1773          home.appendChild(menu);
1774        });
1775      }
1776
1777      window.addEventListener('resize', () => {
1778        for (const [menu, toggle] of pairs) {
1779          if (!menu.classList.contains('dropdown-menu-detached')) continue;
1780          // Crossing into the collapsed navbar mid-open: close it so it reattaches.
1781          if (desktop.matches) place(menu, toggle);
1782          else bootstrap.Dropdown.getInstance(toggle)?.hide();
1783        }
1784      });
1785    })();
1786  </script>
1786
1787  
1787<script>
1788    var beamer_config = {
1789      product_id: 'MzmSqYwo78692',
1790      user_id: "anonymous",
1791      selector: '#beamer-whats-new',
1792      display: "light"
1793    };
1794  </script>
1794
1795  
1795<script type="text/javascript" src="https://app.getbeamer.com/js/beamer-embed.js" defer="defer"></script>
1795
1796  
1796<script defer>
1797    /* =================== THEME =================== */
1798    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
1799    const storageKey = 'darkTheme';
1800    var isHome = false;
1801    // iOS fixed background causes scroll jank — use scroll attachment instead
1802    const _isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
1803      (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
1804    function setTheme(dark) {
1805      document.documentElement.classList.toggle('dark-theme', dark);
1806      document.body.classList.toggle('dark-theme', dark);
1807
1808      // No colors are hardcoded here. Toggling the `.dark-theme` class (above) swaps the
1809      // token values in :root / .dark-theme, and `html, body { background: var(--color-canvas) }`
1810      // repaints the page background. Single source of truth = the CSS tokens in style.css.
1811      const themeBtnIcon = dark ? 'fa-moon' : 'fa-sun';
1812      document.getElementById('theme_button').innerHTML = `<i class="fa-solid ${themeBtnIcon} fa-fw me-2" aria-hidden="true"></i><span class="d-lg-none">Toggle Theme</span>`;
1813      document.body.style.backgroundAttachment = '';
1814
1815
1816      // navbar / site‑icon color tweaks
1817      const navbar = document.getElementById('navbar_id');
1818      navbar.classList.toggle('navbar-dark', dark);
1819      navbar.classList.toggle('navbar-light', !dark);
1820
1821
1822      document.querySelectorAll('.site_icon').forEach(icon => {
1823        icon.src = dark ? '/static/logo.svg' : '/static/logo-grey.svg';
1824      });
1825
1826      // bulk toggle of dark‑theme utility class
1827      const selectors = ['.navbar-collapse', '.form-control', '.table', 'a', 'select', 'ul', '.card', '.dropdown-item', 'thead', 'footer', '.alert'];
1828      selectors.forEach(sel => document.querySelectorAll(sel).forEach(el => el.classList.toggle('dark-theme', dark)));
1829
1830      // Force mobile navbar theme-sync (hard-lock) - ONLY ON MOBILE
1831      if (window.innerWidth <= 991) {
1832        const collapse = document.querySelector('.navbar-collapse');
1833        if (collapse) {
1834          const bg = 'var(--color-bg)';
1835          const txt = 'var(--color-text)';
1836          collapse.style.setProperty('background', bg, 'important');
1837          collapse.querySelectorAll('.nav-link').forEach(link => {
1838            link.style.setProperty('color', txt, 'important');
1839          });
1840        }
1841      } else {
1842        // Reset on desktop
1843        const collapse = document.querySelector('.navbar-collapse');
1844        if (collapse) {
1845          collapse.style.removeProperty('background');
1846          collapse.querySelectorAll('.nav-link').forEach(link => {
1847            link.style.removeProperty('color');
1848          });
1849        }
1850      }
1851
1852      // google sign‑in buttons (if present)
1853      document.querySelectorAll('.google_sign_in').forEach(img => {
1854        const darkSrc = img.id === 'sign_in' ? '/static/sign_in_dark.png' : '/static/sign_up_dark.png';
1855        const lightSrc = img.id === 'sign_in' ? '/static/sign_in_light.png' : '/static/sign_up_light.png';
1856        img.src = dark ? darkSrc : lightSrc;
1857      });
1858      localStorage.setItem(storageKey, dark);
1859      if (typeof setThemeCallback === 'function') {
1860        setThemeCallback(dark);
1861      }
1862
1863      // Update Beamer theme on-the-fly
1864      if (window.Beamer && typeof Beamer.update === 'function') {
1865        Beamer.update({ display: dark ? 'dark' : 'light' });
1866      }
1867    }
1868
1869    function initTheme() {
1870      const stored = localStorage.getItem(storageKey);
1871      const dark = stored ? stored === 'true' : prefersDark.matches || !window.matchMedia; // default dark on small devices
1872
1873      setTheme(dark);
1874    }
1875
1876    function toggleDarkTheme() {
1877      setTheme(!document.body.classList.contains('dark-theme'));
1878    }
1879    function timeSince(time) {
1880      const current = new Date();
1881      const elapsed = (current - new Date(time)) / 1000; // seconds
1882      const secPerMinute = 60;
1883      const secPerHour = secPerMinute * 60;
1884      const secPerDay = secPerHour * 24;
1885      const secPerMonth = secPerDay * 30;
1886      const secPerYear = secPerDay * 365;
1887
1888      if (elapsed < secPerMinute) {
1889        return Math.floor(elapsed) === 1
1890          ? "1 second ago"
1891          : `${Math.floor(elapsed)} seconds ago`;
1892      } else if (elapsed < secPerHour) {
1893        return Math.floor(elapsed / 60) === 1
1894          ? "1 minute ago"
1895          : `${Math.floor(elapsed / 60)} minutes ago`;
1896      } else if (elapsed < secPerDay) {
1897        return Math.floor(elapsed / 3600) === 1
1898          ? "1 hour ago"
1899          : `${Math.floor(elapsed / 3600)} hours ago`;
1900      } else if (elapsed < secPerMonth) {
1901        return Math.floor(elapsed / 86400) === 1
1902          ? "~ 1 day ago"
1903          : `~ ${Math.floor(elapsed / 86400)} days ago`;
1904      } else if (elapsed < secPerYear) {
1905        return Math.floor(elapsed / 2592000) === 1
1906          ? "~ 1 month ago"
1907          : `~ ${Math.floor(elapsed / 2592000)} months ago`;
1908      } else {
1909        return Math.floor(elapsed / 31536000) === 1
1910          ? "~ 1 year ago"
1911          : `~ ${Math.floor(elapsed / 31536000)} years ago`;
1912      }
1913    }
1914
1915    /* =================== MINI-STATS =================== */
1916    let mini_stats_update_interval = null;
1917
1918    async function fetchMiniStats() {
1919      if (document.hidden) return;
1920      const url = "/mini_stats";
1921      let res;
1922      try { res = await fetch(url); } catch { return; }
1923      if (!res.ok) return;
1924
1925      const { last_clip, today_count } = await res.json();
1926
1927      // Update UI immediately on fetch
1928      updateMiniStats(last_clip, today_count);
1929
1930      if (mini_stats_update_interval) {
1931        clearInterval(mini_stats_update_interval);
1932      }
1933
1934      mini_stats_update_interval = setInterval(() => {
1935        updateMiniStats(last_clip, today_count);
1936      }, 5 * 60 * 1000); // 5 minutes
1937
1938      isFirstLoad = false;
1939      return { last_clip, today_count };
1940    }
1941
1942    function updateMiniStats(last_clip, today_count) {
1943      const todayEl = document.getElementById("today_count");
1944      const lastEl = document.getElementById("last_clip_time");
1945      const statsPill = document.getElementById("stats-pill");
1946
1947      if (todayEl) {
1948        todayEl.innerHTML = `<i class='fa-solid fa-fire-pulse me-2'></i> <strong style="font-variant-numeric: tabular-nums;">${today_count}</strong> Clips Captured in 24h`;
1949      }
1950
1951      if (lastEl) {
1952        if (last_clip) {
1953          const ts = timeSince(last_clip.time);
1954          lastEl.textContent = `Latest Clip: ${ts}`;
1955        } else {
1956          lastEl.textContent = `Start the first clip!`;
1957        }
1958      }
1959
1960      // Update stats pill visibility if it exists on the page (home page)
1961      if (statsPill) {
1962        if (today_count > 0) {
1963          statsPill.style.setProperty("display", "inline-flex", "important");
1964        } else {
1965          statsPill.style.setProperty("display", "none", "important");
1966        }
1967      }
1968    }
1969
1970
1971    function showMembershipAlerts() {
1972      
1973
1974    
1975    
1976    
1977    }
1978    /* =================== ALERTS =================== */
1979    // `action` — optional { label, onClick } that renders a button inside the toast,
1980    // for offering an undo instead of making the user hunt for the reverse operation.
1981    function send_alert(message, level, href, duration = 5000, action = null) {
1982      // Ensure a default level if none is provided
1983      if (!level) {
1984        level = 'info';
1985      }
1986
1987      // Create or get the custom alerts container
1988      let customAlertsContainer = document.getElementById('custom-alerts-container');
1989      if (!customAlertsContainer) {
1990        customAlertsContainer = document.createElement('div');
1991        customAlertsContainer.id = 'custom-alerts-container';
1992        customAlertsContainer.setAttribute('role', 'status');
1993        customAlertsContainer.setAttribute('aria-live', 'polite');
1994        customAlertsContainer.setAttribute('aria-atomic', 'false');
1995        document.body.appendChild(customAlertsContainer);
1996      }
1997
1998      // Determine the icon based on the alert level
1999      let iconClass = '';
2000      switch (level) {
2001        case 'danger':
2002          iconClass = 'fa-solid fa-circle-exclamation';
2003          break;
2004        case 'success':
2005          iconClass = 'fa-solid fa-circle-check';
2006          break;
2007        case 'warning':
2008          iconClass = 'fa-solid fa-triangle-exclamation';
2009          break;
2010        case 'info':
2011          iconClass = 'fa-solid fa-circle-info';
2012          break;
2013        default:
2014          iconClass = 'fa-solid fa-bell';
2015          break;
2016      }
2017
2018      // Create the alert element
2019      const alertElement = document.createElement('div');
2020      alertElement.className = `alert custom-toast toast-${level} m-0`;
2021
2022      let messageBody = message;
2023      if (href) {
2024        messageBody = `<a href="${href}" class="alert-link text-decoration-none">${message}</a>`;
2025      }
2026
2027      alertElement.innerHTML = `
2028        <i class="${iconClass} toast-icon" aria-hidden="true"></i>
2029        <div class="toast-message">${messageBody}</div>
2030      `;
2031
2032      // Apply initial animation styles for entry
2033      alertElement.style.opacity = '0';
2034      alertElement.style.transform = 'translateY(20px)';
2035
2036      // Append the new alert to the custom container
2037      customAlertsContainer.appendChild(alertElement);
2038
2039      // Trigger the entry animation after appending
2040      setTimeout(() => {
2041        alertElement.style.opacity = '1';
2042        alertElement.style.transform = 'translateY(0) translateX(0)';
2043      }, 10);
2044
2045      const dismissAlert = () => {
2046        alertElement.style.opacity = '0';
2047        alertElement.style.transform = 'scale(0.9)';
2048        setTimeout(() => {
2049          if (alertElement.parentNode) {
2050            alertElement.parentNode.removeChild(alertElement);
2051          }
2052        }, 400); // Wait for transition
2053      };
2054
2055      let dismissTimeout;
2056
2057      const startDismissTimer = () => {
2058        dismissTimeout = setTimeout(() => {
2059          dismissAlert();
2060        }, duration);
2061      };
2062
2063      startDismissTimer();
2064
2065      if (action && action.label) {
2066        const actionBtn = document.createElement('button');
2067        actionBtn.type = 'button';
2068        actionBtn.className = 'toast-action';
2069        actionBtn.textContent = action.label;
2070        actionBtn.addEventListener('click', () => {
2071          clearTimeout(dismissTimeout);
2072          dismissAlert();
2073          if (typeof action.onClick === 'function') action.onClick();
2074        });
2075        alertElement.appendChild(actionBtn);
2076      }
2077
2078      // Desktop hover pause
2079      alertElement.addEventListener('mouseenter', () => {
2080        clearTimeout(dismissTimeout);
2081        alertElement.style.opacity = '1';
2082        alertElement.style.transform = 'translateY(0) translateX(0)';
2083      });
2084
2085      alertElement.addEventListener('mouseleave', () => {
2086        startDismissTimer();
2087      });
2088
2089      // Swipe to dismiss logic for mobile
2090      let startX = 0;
2091      let currentX = 0;
2092
2093      alertElement.addEventListener('touchstart', (e) => {
2094        startX = e.touches[0].clientX;
2095        alertElement.style.transition = 'none'; // Disable transition for 1:1 finger tracking
2096        clearTimeout(dismissTimeout);
2097      }, { passive: true });
2098
2099      alertElement.addEventListener('touchmove', (e) => {
2100        currentX = e.touches[0].clientX;
2101        const diffX = currentX - startX;
2102        // Move the toast with finger
2103        alertElement.style.transform = `translateX(${diffX}px)`;
2104        // Fade out slightly as it moves away
2105        alertElement.style.opacity = Math.max(0, 1 - Math.abs(diffX) / window.innerWidth);
2106      }, { passive: true });
2107
2108      alertElement.addEventListener('touchend', (e) => {
2109        const diffX = currentX - startX;
2110        // Restore transition
2111        alertElement.style.transition = 'opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1), transform 0.4s cubic-bezier(0.16, 1, 0.3, 1)';
2112
2113        if (Math.abs(diffX) > 80) {
2114          // Swipe threshold reached, fly off screen and dismiss
2115          const direction = diffX > 0 ? 1 : -1;
2116          alertElement.style.transform = `translateX(${direction * window.innerWidth}px)`;
2117          alertElement.style.opacity = '0';
2118          setTimeout(() => {
2119            if (alertElement.parentNode) {
2120              alertElement.parentNode.removeChild(alertElement);
2121            }
2122          }, 400);
2123        } else {
2124          // Snap back
2125          alertElement.style.transform = 'translateY(0) translateX(0)';
2126          alertElement.style.opacity = '1';
2127          startDismissTimer(); // Resume dismiss timer
2128        }
2129      });
2130    }
2131
2132
2133    /* =================== INIT =================== */
2134    document.addEventListener('DOMContentLoaded', () => {
2135      initTheme();
2136      fetchMiniStats();
2137      let miniStatsTimer = setInterval(fetchMiniStats, 60_000);
2138
2139      document.addEventListener('visibilitychange', () => {
2140        if (document.hidden) {
2141          clearInterval(miniStatsTimer);
2142          miniStatsTimer = null;
2143        } else {
2144          fetchMiniStats();
2145          miniStatsTimer = miniStatsTimer || setInterval(fetchMiniStats, 60_000);
2146        }
2147      });
2148      if (typeof showMembershipAlerts === 'function') {
2149        showMembershipAlerts();
2150      }
2151
2152
2153
2154      // Navbar logo hover clap
2155      (function() {
2156        var wrap  = document.getElementById('site_icon');
2157        var svg   = document.getElementById('_nbl_svg');
2158        var stick = document.getElementById('_nbl_stick');
2159        if (!wrap || !svg || !stick) return;
2160        var busy = false;
2161        wrap.addEventListener('mouseenter', function() {
2162          if (busy) return;
2163          busy = true;
2164          stick.classList.add('_nbl_play');
2165          svg.classList.add('_nbl_play');
2166          setTimeout(function() {
2167            stick.classList.remove('_nbl_play');
2168            svg.classList.remove('_nbl_play');
2169            busy = false;
2170          }, 2600);
2171        });
2172      })();
2173
2174      // Auto-collapse burger menu on outside click
2175      document.addEventListener('click', function (event) {
2176        const navbarCollapse = document.getElementById('navbarSupportedContent');
2177        const navbarToggler = document.querySelector('.navbar-toggler');
2178        if (navbarCollapse && navbarCollapse.classList.contains('show')) {
2179          if (!navbarCollapse.contains(event.target) && !navbarToggler.contains(event.target)) {
2180            new bootstrap.Collapse(navbarCollapse).hide();
2181          }
2182        }
2183      });
2184
2185      // Service Worker Registration for PWA
2186      if ('serviceWorker' in navigator) {
2187        window.addEventListener('load', () => {
2188          navigator.serviceWorker.register('/static/sw.js')
2189            .then(reg => console.log('[PWA] Service Worker registered:', reg.scope))
2190            .catch(err => console.log('[PWA] Service Worker registration failed:', err));
2191        });
2192      }
2193    });
2194
2195  </script>
2195
2196  
2197<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
2197
2198
2199  
2200<script>
2201
2202    /* ── Three.js Star Warp for Login Background ── */
2203    let loginSceneData = null;
2204
2205    function initLoginScene() {
2206        const canvas = document.getElementById('login-canvas');
2207        if (!canvas) return;
2208
2209        // three.js is loaded from a CDN that can fail on some networks — degrade gracefully.
2210        if (typeof THREE === 'undefined') { canvas.style.display = 'none'; return; }
2211
2212        if (document.documentElement.classList.contains('no-gpu') ||
2213            window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
2214            canvas.style.display = 'none';
2215            return;
2216        }
2217
2218        const scene = new THREE.Scene();
2219        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
2220        const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
2221
2222        renderer.setSize(window.innerWidth, window.innerHeight);
2223        renderer.setPixelRatio(window.innerWidth <= 768
2224            ? Math.min(window.devicePixelRatio, 1.5)
2225            : Math.min(window.devicePixelRatio, 2));
2226
2227        const starCount = 1200;
2228        const positions = new Float32Array(starCount * 3);
2229        const velocities = new Float32Array(starCount);
2230        for (let i = 0; i < starCount; i++) {
2231            positions[i * 3]     = (Math.random() - 0.5) * 20;
2232            positions[i * 3 + 1] = (Math.random() - 0.5) * 20;
2233            positions[i * 3 + 2] = (Math.random() - 1.0) * 15;
2234            velocities[i] = 0.006 + Math.random() * 0.01;
2235        }
2236        const geometry = new THREE.BufferGeometry();
2237        geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
2238
2239        const isDark = document.body.classList.contains('dark-theme');
2240        const material = new THREE.PointsMaterial({
2241            color: isDark ? 0x2293f2 : 0x1e293b,
2242            size: 0.04,
2243            transparent: true,
2244            opacity: isDark ? 0.4 : 0.7,
2245            blending: isDark ? THREE.AdditiveBlending : THREE.NormalBlending
2246        });
2247
2248        const starField = new THREE.Points(geometry, material);
2249        scene.add(starField);
2250        camera.position.z = 5;
2251
2252        let mouseX = 0, mouseY = 0;
2253        const onMouseMove = (e) => {
2254            mouseX = (e.clientX / window.innerWidth - 0.5) * 0.3;
2255            mouseY = (e.clientY / window.innerHeight - 0.5) * 0.3;
2256        };
2257        window.addEventListener('mousemove', onMouseMove);
2258
2259        const onResize = () => {
2260            camera.aspect = window.innerWidth / window.innerHeight;
2261            camera.updateProjectionMatrix();
2262            renderer.setSize(window.innerWidth, window.innerHeight);
2263        };
2264        window.addEventListener('resize', onResize);
2265
2266        loginSceneData = { scene, camera, renderer, material, geometry, velocities, starField };
2267
2268        function animate() {
2269            requestAnimationFrame(animate);
2270            const pos = geometry.attributes.position.array;
2271            for (let i = 0; i < starCount; i++) {
2272                pos[i * 3 + 2] += velocities[i];
2273                if (pos[i * 3 + 2] > 5) pos[i * 3 + 2] = -15;
2274            }
2275            geometry.attributes.position.needsUpdate = true;
2276            starField.rotation.z += 0.0002;
2277            camera.position.x += (mouseX - camera.position.x) * 0.05;
2278            camera.position.y += (-mouseY - camera.position.y) * 0.05;
2279            camera.lookAt(0, 0, 0);
2280            renderer.render(scene, camera);
2281        }
2282        animate();
2283    }
2284
2285    window.setThemeCallback = (dark) => {
2286        if (loginSceneData) {
2287            loginSceneData.material.color.setHex(dark ? 0x2293f2 : 0x1e293b);
2288            loginSceneData.material.opacity = dark ? 0.4 : 0.7;
2289            loginSceneData.material.blending = dark ? THREE.AdditiveBlending : THREE.NormalBlending;
2290        }
2291    };
2292
2293    document.addEventListener('DOMContentLoaded', () => {
2294        initLoginScene();
2295    });
2296</script>
2296
2297
2298  
2298<script>
2299  (function(){
2300    var s=document.createElement('script');
2301    s.async=true;
2302    s.src='https://unpkg.com/web-vitals@3/dist/web-vitals.iife.js';
2303    s.onload=function(){
2304      if(!window.webVitals)return;
2305      function sv(m){
2306        try{navigator.sendBeacon('/api/vitals',JSON.stringify({n:m.name,v:m.value,p:location.pathname}));}catch(e){}
2307      }
2308      webVitals.onFCP(sv);webVitals.onLCP(sv);webVitals.onCLS(sv);webVitals.onINP(sv);webVitals.onTTFB(sv);
2309    };
2310    document.head.appendChild(s);
2311  })();
2312  </script>
2312
2313
2314  <!-- ========= TERMINAL COPY BUTTONS =========
2315       Adds a "Copy" button to every .terminal-container (command/code blocks on
2316       get-started, guides, yt-dlp builder…). Replaces the undiscoverable
2317       click-anywhere-to-copy with an obvious affordance + copied-state feedback.
2318       Terminals are always dark, so this light-on-glass button reads in both themes. -->
2319  <style>
2320    .terminal-container { position: relative; }
2321    .ss-term-copy {
2322      display: inline-flex;
2323      align-items: center;
2324      gap: 6px;
2325      padding: 4px 9px;
2326      font-size: 0.7rem;
2327      font-weight: 600;
2328      line-height: 1;
2329      font-family: inherit;
2330      color: #cbd5e1;
2331      background: rgba(255, 255, 255, 0.08);
2332      border: 1px solid rgba(255, 255, 255, 0.14);
2333      border-radius: var(--radius-sm);
2334      cursor: pointer;
2335      transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.1s ease;
2336    }
2337    .ss-term-copy:hover { background: rgba(255, 255, 255, 0.16); color: #fff; }
2338    .ss-term-copy:active { transform: scale(0.94); }
2339    .ss-term-copy.copied {
2340      color: #4ade80;
2341      border-color: rgba(74, 222, 128, 0.4);
2342      background: rgba(74, 222, 128, 0.14);
2343    }
2344    /* Floating variant for terminals with no title bar — reserve room so the
2345       button never overlaps the first line of the command. */
2346    .ss-term-copy--float { position: absolute; top: 8px; right: 8px; z-index: 3; }
2347    .terminal-container.has-floating-copy .terminal-body { padding-top: 2.5rem; }
2348  </style>
2349  
2349<script>
2350    (function () {
2351      function addCopyButtons() {
2352        document.querySelectorAll('.terminal-container').forEach(function (term) {
2353          if (term.querySelector('.ss-term-copy')) return;          // already enhanced
2354          var body = term.querySelector('.terminal-body');
2355          if (!body) return;
2356          var header = term.querySelector('.terminal-header');
2357
2358          var btn = document.createElement('button');
2359          btn.type = 'button';
2360          btn.className = 'ss-term-copy';
2361          btn.setAttribute('aria-label', 'Copy to clipboard');
2362          btn.innerHTML = '<i class="fa-solid fa-copy" aria-hidden="true"></i><span>Copy</span>';
2363
2364          if (header) {
2365            // sit at the right of the title bar; only self-align when the header
2366            // has no existing right-aligned element (e.g. a "Nightbot" label).
2367            if (!header.querySelector('.ms-auto')) btn.style.marginLeft = 'auto';
2368            header.appendChild(btn);
2369          } else {
2370            btn.classList.add('ss-term-copy--float');
2371            term.classList.add('has-floating-copy');
2372            term.appendChild(btn);
2373          }
2374
2375          btn.addEventListener('click', function (e) {
2376            e.stopPropagation();
2377            var text = (body.innerText || '').trim();
2378            navigator.clipboard.writeText(text).then(function () {
2379              btn.classList.add('copied');
2380              btn.innerHTML = '<i class="fa-solid fa-check" aria-hidden="true"></i><span>Copied</span>';
2381              setTimeout(function () {
2382                btn.classList.remove('copied');
2383                btn.innerHTML = '<i class="fa-solid fa-copy" aria-hidden="true"></i><span>Copy</span>';
2384              }, 1800);
2385            }).catch(function () {});
2386          });
2387        });
2388      }
2389      document.addEventListener('DOMContentLoaded', addCopyButtons);
2390      if (document.readyState !== 'loading') addCopyButtons();
2391    })();
2392  </script>
2392
2393
2394  <!-- ========= PALETTE DROPDOWN (wiring) =========
2395       Lets a signed-in user pick an accent palette from the navbar. The choice is
2396       stored in localStorage (client-side only — does NOT change the CSS file or
2397       affect other users) and re-applied pre-paint by the head script. -->
2398  <style>
2399    .palette-dot {
2400      display: inline-block;
2401      width: 12px;
2402      height: 12px;
2403      border-radius: 50%;
2404      margin-inline-end: 8px;
2405      vertical-align: middle;
2406      box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
2407    }
2408  </style>
2409  
2409<script>
2410    (function () {
2411      function apply(h, s, l, rgb) {
2412        var st = document.documentElement.style;
2413        if (h === null) {
2414          st.removeProperty('--accent-h'); st.removeProperty('--accent-s');
2415          st.removeProperty('--accent-l'); st.removeProperty('--color-primary-rgb');
2416          localStorage.removeItem('ss_palette');
2417          return;
2418        }
2419        st.setProperty('--accent-h', h); st.setProperty('--accent-s', s);
2420        st.setProperty('--accent-l', l); st.setProperty('--color-primary-rgb', rgb);
2421        localStorage.setItem('ss_palette', JSON.stringify({ h: h, s: s, l: l, rgb: rgb }));
2422      }
2423      function wire() {
2424        document.querySelectorAll('.palette-opt').forEach(function (opt) {
2425          if (opt._wired) return; opt._wired = true;
2426          opt.addEventListener('click', function (e) {
2427            e.preventDefault();
2428            if (opt.dataset.reset) apply(null);
2429            else apply(opt.dataset.h, opt.dataset.s, opt.dataset.l, opt.dataset.rgb);
2430          });
2431        });
2432      }
2433      document.addEventListener('DOMContentLoaded', wire);
2434      if (document.readyState !== 'loading') wire();
2435    })();
2436  </script>
2436
2437</body>
2438
2439</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.