PageSourceSearch

https://streamportal.pages.dev/

html streamportal.pages.dev collected 2026-10-03 05:56:40 UTC 61,745 bytes, 1,712 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
5<title>StreamPortal</title>
6<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
7<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
8
9<style>
10:root {
11  --bg: #07090e;
12  --card: #141720;
13  --card-hover: #1e2330;
14  --border: rgba(255, 255, 255, 0.08);
15  --text: #f5f5f7;
16  --muted: #9499a8;
17  --accent: #0066cc;
18  --accent-glow: rgba(0, 102, 204, 0.35);
19  --live: #ff3b30; 
20  --live-glow: rgba(255, 59, 48, 0.3);
21  --apple-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
22}
23
24* { box-sizing: border-box; margin: 0; padding: 0; }
25
26body {
27  font-family: 'Inter', -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, sans-serif;
28  background: linear-gradient(180deg, #07090e 0%, #030406 100%);
29  color: var(--text);
30  min-height: 100vh;
31  width: 100%;
32  overflow-x: hidden;
33  -webkit-font-smoothing: antialiased;
34  -moz-osx-font-smoothing: grayscale;
35}
36
37/* ===== HEADER ===== */
38header {
39  position: sticky;
40  top: 0;
41  z-index: 2000;
42  background: rgba(7, 9, 14, 0.75);
43  backdrop-filter: saturate(180%) blur(24px);
44  -webkit-backdrop-filter: saturate(180%) blur(24px);
45  border-bottom: 1px solid var(--border);
46  width: 100%;
47}
48
49.header-inner {
50  max-width: 1400px;
51  margin: auto;
52  padding: 14px 24px;
53  display: grid;
54  grid-template-columns: 1fr auto 1fr;
55  align-items: center;
56  gap: 20px;
57  width: 100%;
58}
59
60.logo {
61  font-weight: 800;
62  font-size: 22px;
63  letter-spacing: -0.8px;
64  user-select: none;
65  justify-self: start;
66  display: flex;
67  align-items: center;
68  color: #fff;
69  text-decoration: none;
70}
71.logo span { 
72  color: var(--accent);
73}
74
75/* SEARCH BAR */
76.search-container {
77  display: flex;
78  align-items: center;
79  position: relative;
80  width: 420px;
81  max-width: 100%;
82  justify-self: center;
83}
84
85.search-wrap {
86  position: relative;
87  width: 100%;
88  display: flex;
89  align-items: center;
90  padding: 12px 16px 12px 42px;
91  border-radius: 999px;
92  background: rgba(255, 255, 255, 0.06);
93  border: 1px solid transparent;
94  transition: all 0.3s var(--apple-ease);
95}
96
97.search-wrap:focus-within {
98  background: rgba(255, 255, 255, 0.1);
99  border-color: var(--accent);
100  box-shadow: 0 0 0 4px var(--accent-glow);
101}
102
103.search-wrap input {
104  width: 100%;
105  background: transparent;
106  border: none;
107  outline: none;
108  color: var(--text);
109  font-size: 15px;
110  font-weight: 500;
111  min-width: 0;
112}
113
114.search-wrap input::placeholder {
115  color: var(--muted);
116}
117
118.search-wrap::before {
119  content: "";
120  position: absolute;
121  left: 16px;
122  top: 50%;
123  transform: translateY(-50%);
124  width: 16px;
125  height: 16px;
126  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239499a8' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E") no-repeat center / contain;
127}
128
129#clearSearch {
130  background: none;
131  border: none;
132  color: var(--muted);
133  font-size: 20px;
134  cursor: pointer;
135  padding: 0 4px;
136  opacity: 0;
137  pointer-events: none;
138  transition: opacity 0.2s;
139}
140
141.search-modal-dropdown {
142  position: absolute;
143  top: calc(100% + 12px);
144  left: 0;
145  width: 100%;
146  max-height: 360px;
147  background: rgba(20, 23, 32, 0.95);
148  backdrop-filter: blur(20px);
149  -webkit-backdrop-filter: blur(20px);
150  border: 1px solid var(--border);
151  border-radius: 16px;
152  box-shadow: 0 24px 48px rgba(0,0,0,0.8);
153  overflow-y: auto;
154  display: none;
155  z-index: 99999;
156  scrollbar-width: none;
157  animation: dropdownFade 0.3s var(--apple-ease);
158}
159
160@keyframes dropdownFade {
161  from { opacity: 0; transform: translateY(-10px) scale(0.98); }
162  to { opacity: 1; transform: translateY(0) scale(1); }
163}
164
165.search-modal-dropdown::-webkit-scrollbar { display: none; }
166.search-modal-dropdown.active { display: block; }
167
168.search-modal-item {
169  display: flex;
170  align-items: center;
171  gap: 14px;
172  padding: 12px 16px;
173  cursor: pointer;
174  border-bottom: 1px solid rgba(255,255,255,0.04);
175  transition: background 0.2s;
176}
177.search-modal-item:hover { background: rgba(255, 255, 255, 0.06); }
178.search-modal-item img { width: 56px; height: 32px; object-fit: cover; border-radius: 6px; background: #000; flex-shrink: 0; }
179.search-modal-item .title { font-size: 14px; font-weight: 600; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
180.search-modal-item .tag { font-size: 12px; color: var(--muted); font-weight: 500; margin-top: 2px;}
181
182.header-actions {
183  display: flex;
184  align-items: center;
185  gap: 10px;
186  justify-self: end;
187}
188
189.multiview-btn, .discord-btn {
190  padding: 10px 20px;
191  border-radius: 999px;
192  font-size: 14px;
193  font-weight: 600;
194  color: #fff;
195  cursor: pointer;
196  border: none;
197  transition: all 0.3s var(--apple-ease);
198  text-decoration: none;
199  display: inline-flex;
200  align-items: center;
201  gap: 6px;
202  white-space: nowrap;
203}
204
205/* Hidden by default on small screens/mobile, shown only on PC/larger screens */
206.multiview-btn {
207  display: none;
208  background: var(--accent);
209  box-shadow: 0 4px 14px var(--accent-glow);
210}
211@media (min-width: 1024px) {
212  .multiview-btn {
213    display: inline-flex;
214  }
215}
216.multiview-btn:hover { 
217  transform: scale(1.03);
218  background: #0055b3;
219  box-shadow: 0 6px 20px var(--accent-glow);
220}
221
222.discord-btn {
223  background: #5865F2;
224  box-shadow: 0 4px 14px rgba(88, 101, 242, 0.35);
225}
226.discord-btn:hover {
227  transform: scale(1.03);
228  background: #4752C4;
229  box-shadow: 0 6px 20px rgba(88, 101, 242, 0.4);
230}
231
232/* ===== HERO COMPONENT ===== */
233.hero {
234  position: relative;
235  min-height: 45vh;
236  display: flex;
237  align-items: center;
238  justify-content: center;
239  overflow: hidden;
240  padding: 60px 24px;
241  width: 100%;
242}
243.hero-bg {
244  position: absolute;
245  inset: 0;
246  background: 
247    linear-gradient(to top, #07090e 0%, rgba(7,9,14,0.3) 100%),
248    radial-gradient(circle at 50% 0%, var(--accent-glow), transparent 60%),
249    url('https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR4MyNgvVqalCBdUOz55t4X_Ef3ALCLN4nHxl4ede4XlIKAL5rSy3Bewik&s=10') center/cover no-repeat;
250  z-index: 0;
251  opacity: 0.6;
252}
253.hero-content { 
254  position: relative; 
255  z-index: 1; 
256  max-width: 800px; 
257  width: 100%;
258  display: flex;
259  flex-direction: column;
260  align-items: center;
261  text-align: center;
262}
263.hero-badge {
264  display: inline-flex;
265  align-items: center;
266  gap: 8px;
267  padding: 6px 14px;
268  border-radius: 999px;
269  background: rgba(255, 255, 255, 0.08);
270  border: 1px solid rgba(255, 255, 255, 0.15);
271  color: #fff;
272  font-size: 11.5px;
273  font-weight: 700;
274  margin: 0 auto 20px auto;
275  letter-spacing: 1px;
276  text-transform: uppercase;
277  backdrop-filter: blur(12px);
278  text-align: center;
279}
280.hero-badge::before {
281  content: "";
282  width: 6px;
283  height: 6px;
284  background: var(--live);
285  border-radius: 50%;
286  box-shadow: 0 0 10px var(--live);
287  flex-shrink: 0;
288}
289.hero h1 { 
290  font-size: clamp(32px, 6vw, 64px); 
291  font-weight: 800; 
292  line-height: 1.05; 
293  margin-bottom: 16px; 
294  width: 100%; 
295  letter-spacing: -1.5px;
296  color: #fff;
297}
298.hero p { color: var(--muted); font-size: clamp(15px, 2vw, 17px); max-width: 600px; margin: 0 auto 32px auto; line-height: 1.5; font-weight: 400; }
299.hero-buttons { display: flex; justify-content: center; gap: 14px; width: 100%; }
300.hero-btn { 
301  padding: 14px 28px; 
302  font-weight: 600; 
303  font-size: 15px;
304  border-radius: 999px; 
305  border: none; 
306  cursor: pointer; 
307  transition: all 0.3s var(--apple-ease); 
308}
309.hero-btn.primary { 
310  background: var(--accent); 
311  color: #fff; 
312  box-shadow: 0 8px 24px var(--accent-glow);
313}
314.hero-btn.primary:hover { 
315  transform: scale(1.04); 
316  background: #0055b3;
317  box-shadow: 0 10px 30px var(--accent-glow);
318}
319
320/* ===== MAIN CONTENT GRID ===== */
321main { max-width: 1400px; margin: auto; padding: 40px 24px; width: 100%; }
322
323#tabs { 
324  display: flex; 
325  justify-content: flex-start;
326  @media(min-width: 768px) { justify-content: center; }
327  gap: 10px; 
328  overflow-x: auto;
329  padding: 0 4px 16px 4px;
330  margin-bottom: 24px; 
331  scrollbar-width: thin;
332  scrollbar-color: var(--accent) rgba(255, 255, 255, 0.05);
333  width: 100%;
334  -webkit-overflow-scrolling: touch;
335}
336
337#tabs::-webkit-scrollbar {
338  height: 4px;
339  display: block;
340}
341#tabs::-webkit-scrollbar-track {
342  background: rgba(255, 255, 255, 0.04);
343  border-radius: 4px;
344}
345#tabs::-webkit-scrollbar-thumb {
346  background: var(--accent);
347  border-radius: 4px;
348}
349
350.tab {
351  padding: 10px 20px;
352  border-radius: 999px;
353  cursor: pointer;
354  font-size: 14px;
355  font-weight: 600;
356  color: var(--muted);
357  background: rgba(255, 255, 255, 0.04);
358  border: 1px solid transparent;
359  transition: all 0.3s var(--apple-ease);
360  white-space: nowrap;
361  flex-shrink: 0;
362}
363.tab:hover { 
364  color: #fff; 
365  background: rgba(255, 255, 255, 0.08);
366}
367.tab.active { 
368  color: #fff; 
369  background: var(--accent);
370  box-shadow: 0 4px 14px var(--accent-glow);
371}
372
373#streams {
374  display: grid;
375  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
376  gap: 28px;
377  width: 100%;
378}
379
380/* Netflix Style Cards */
381.stream {
382  background: transparent;
383  border-radius: 12px;
384  overflow: hidden;
385  cursor: pointer;
386  display: flex;
387  flex-direction: column;
388  position: relative;
389  transition: all 0.4s var(--apple-ease);
390  width: 100%;
391}
392
393.poster-wrapper {
394  position: relative;
395  width: 100%;
396  aspect-ratio: 16/9;
397  background: #10131c;
398  border-radius: 12px;
399  overflow: hidden;
400  box-shadow: 0 4px 20px rgba(0,0,0,0.5);
401  border: 1px solid transparent;
402  transition: all 0.4s var(--apple-ease);
403}
404
405.poster { 
406  width: 100%; 
407  height: 100%; 
408  object-fit: cover; 
409  transition: transform 0.6s var(--apple-ease);
410}
411
412.stream:hover .poster-wrapper {
413  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.8);
414  border: 1px solid rgba(0, 102, 204, 0.4);
415}
416.stream:hover .poster {
417  transform: scale(1.06);
418}
419.stream:hover {
420  transform: translateY(-6px);
421}
422
423.stream-live-badge {
424  position: absolute;
425  top: 12px;
426  left: 12px;
427  background: var(--live);
428  padding: 4px 10px;
429  border-radius: 6px;
430  display: flex;
431  align-items: center;
432  gap: 6px;
433  font-size: 11px;
434  font-weight: 800;
435  color: #fff;
436  letter-spacing: 0.5px;
437  z-index: 2;
438  box-shadow: 0 4px 12px rgba(255, 59, 48, 0.4);
439}
440.stream-live-badge span {
441  width: 6px;
442  height: 6px;
443  background: #fff;
444  border-radius: 50%;
445  animation: pulseLive 1.5s infinite;
446  flex-shrink: 0;
447}
448@keyframes pulseLive {
449  0% { opacity: 1; transform: scale(1); }
450  50% { opacity: 0.4; transform: scale(0.8); }
451  100% { opacity: 1; transform: scale(1); }
452}
453
454/* Viewer Badges */
455.card-viewers-badge {
456  position: absolute;
457  top: 12px;
458  right: 12px;
459  background: rgba(0, 0, 0, 0.7);
460  backdrop-filter: blur(8px);
461  -webkit-backdrop-filter: blur(8px);
462  padding: 4px 8px;
463  border-radius: 6px;
464  display: inline-flex;
465  align-items: center;
466  gap: 5px;
467  font-size: 11px;
468  font-weight: 700;
469  color: #fff;
470  border: 1px solid rgba(255, 255, 255, 0.1);
471  z-index: 2;
472}
473
474.card-viewers-badge .dot {
475  width: 6px;
476  height: 6px;
477  background: #ff3b30;
478  border-radius: 50%;
479  box-shadow: 0 0 6px #ff3b30;
480}
481
482.theater-viewers-pill {
483  display: inline-flex;
484  align-items: center;
485  gap: 6px;
486  background: rgba(255, 59, 48, 0.15);
487  border: 1px solid rgba(255, 59, 48, 0.3);
488  color: #ff3b30;
489  padding: 8px 14px;
490  border-radius: 999px;
491  font-size: 13px;
492  font-weight: 700;
493}
494
495.theater-viewers-pill .dot {
496  width: 7px;
497  height: 7px;
498  background: #ff3b30;
499  border-radius: 50%;
500  animation: pulseLive 1.5s infinite;
501}
502
503.info { padding: 16px 4px 0 4px; flex: 1; display: flex; flex-direction: column; justify-content: flex-start; gap: 8px; width: 100%; }
504.info strong { display: block; font-size: 16px; font-weight: 700; color: #fff; line-height: 1.3; letter-spacing: -0.2px; word-break: break-word;}
505.meta { font-size: 13px; color: var(--muted); display: flex; align-items: center; justify-content: space-between; font-weight: 500; gap: 8px; }
506
507.countdown { font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 6px; background: rgba(255,255,255,0.06); display: inline-block; letter-spacing: 0.3px; color: #fff; white-space: nowrap;}
508.countdown.live { color: var(--live); background: rgba(255, 59, 48, 0.15); font-weight: 700; }
509.countdown.upcoming { color: var(--text); }
510
511/* ===== THEATER / SIDEBAR ===== */
512#playerView { display: none; padding: 0 0 24px 0; width: 100%; }
513#playerView.active { display: block; }
514
515.player-layout {
516  display: grid;
517  grid-template-columns: 340px 1fr;
518  gap: 24px;
519  align-items: start;
520  width: 100%;
521}
522
523#sideStreams {
524  display: flex;
525  flex-direction: column;
526  gap: 16px;
527  max-height: calc(100vh - 120px); 
528  overflow-y: auto;
529  padding-right: 8px;
530  scrollbar-width: thin;
531  scrollbar-color: rgba(255,255,255,0.2) transparent;
532}
533
534#sideStreams::-webkit-scrollbar { width: 4px; }
535#sideStreams::-webkit-scrollbar-track { background: transparent; }
536#sideStreams::-webkit-scrollbar-thumb { background-color: rgba(255,255,255,0.2); border-radius: 4px; }
537
538#sideStreams .stream {
539  display: flex;
540  flex-direction: row;
541  height: 84px;
542  flex-shrink: 0;
543  padding: 0;
544  gap: 14px;
545  background: transparent;
546  border-radius: 10px;
547  align-items: center;
548  width: 100%;
549}
550
551#sideStreams .poster-wrapper {
552  width: 130px;
553  height: 100%;
554  aspect-ratio: 16/9;
555  border-radius: 10px;
556  flex-shrink: 0;
557}
558
559#sideStreams .info {
560  padding: 0;
561  flex: 1;
562  display: flex;
563  flex-direction: column;
564  justify-content: center;
565  gap: 6px;
566  overflow: hidden;
567  min-width: 0;
568}
569
570#sideStreams .info strong {
571  font-size: 13px;
572  margin: 0;
573  white-space: normal;
574  display: -webkit-box;
575  -webkit-line-clamp: 2;
576  -webkit-box-orient: vertical;
577  overflow: hidden;
578}
579
580#sideStreams .meta {
581  justify-content: flex-start;
582  gap: 8px;
583  font-size: 11px;
584}
585
586.player-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; gap: 16px; width: 100%; flex-wrap: wrap; }
587.player-nav-group { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
588.player-header h2 { font-size: 20px; font-weight: 700; color: #fff; letter-spacing: -0.4px; word-break: break-word; }
589
590.back-btn { 
591  background: rgba(255,255,255,0.06); 
592  border: none; 
593  color: white; 
594  padding: 10px 16px; 
595  border-radius: 999px; 
596  cursor: pointer; 
597  font-weight: 600;
598  font-size: 14px;
599  transition: all 0.2s var(--apple-ease);
600  display: flex;
601  align-items: center;
602  gap: 6px;
603  flex-shrink: 0;
604}
605.back-btn:hover { background: rgba(255,255,255,0.12); transform: scale(1.05); }
606
607.iframe-wrap { width: 100%; aspect-ratio: 16/9; background: #000; border-radius: 16px; overflow: hidden; position: relative; border: 1px solid rgba(255,255,255,0.05); box-shadow: 0 20px 60px rgba(0,0,0,0.8); }
608.iframe-wrap iframe { width: 100%; height: 100%; border: none; }
609
610.now-card {
611  padding: 16px; 
612  background: var(--card); 
613  border-radius: 16px; 
614  margin-bottom: 8px;
615  display: flex; 
616  flex-direction: column; 
617  gap: 6px;
618  border: 1px solid rgba(0, 102, 204, 0.3);
619  width: 100%;
620}
621.now-label { font-size: 11px; font-weight: 800; color: var(--accent); letter-spacing: 1px; text-transform: uppercase; }
622.now-title { font-size: 15px; font-weight: 700; color: white; white-space: normal; line-height: 1.4; word-break: break-word; }
623.now-meta { font-size: 12px; color: var(--muted); }
624
625.stream-loader { position: absolute; inset: 0; background: rgba(7,9,14,0.9); display: flex; align-items: center; justify-content: center; z-index: 100; backdrop-filter: blur(10px); }
626.spinner { width: 40px; height: 40px; border: 3px solid rgba(255,255,255,0.1); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
627@keyframes spin { to { transform: rotate(360deg); } }
628
629/* ===== SOURCE BACKUP MODALS ===== */
630.source-modal { position: fixed; inset: 0; background: rgba(3,4,6,0.85); backdrop-filter: saturate(150%) blur(20px); z-index: 3000; display: flex; align-items: center; justify-content: center; padding: 16px; animation: modalFade 0.3s var(--apple-ease); }
631@keyframes modalFade { from { opacity: 0; transform: scale(1.05); } to { opacity: 1; transform: scale(1); } }
632
633.source-box { 
634  background: var(--card); 
635  padding: 24px; 
636  border-radius: 24px; 
637  width: 420px; 
638  max-width: 100%; 
639  max-height: 75vh;
640  overflow-y: auto;
641  box-shadow: 0 40px 80px rgba(0,0,0,0.9);
642  border: 1px solid rgba(0, 102, 204, 0.3);
643}
644
645.source-box::-webkit-scrollbar { width: 4px; }
646.source-box::-webkit-scrollbar-track { background: transparent; }
647.source-box::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 4px; }
648.source-box h3 { margin-bottom: 20px; font-size: 18px; font-weight: 700; letter-spacing: -0.4px; }
649
650.source-option { 
651  display: block; 
652  width: 100%; 
653  padding: 14px 16px; 
654  background: rgba(255,255,255,0.03); 
655  border: 1px solid transparent;
656  border-radius: 12px; 
657  color: white; 
658  cursor: pointer; 
659  text-align: left; 
660  margin-bottom: 10px; 
661  font-weight: 600; 
662  font-size: 14px;
663  transition: all 0.3s var(--apple-ease);
664}
665.source-option:hover { background: rgba(255,255,255,0.06); transform: scale(1.02); }
666.source-option.active { background: var(--accent); color: #fff; box-shadow: 0 4px 14px var(--accent-glow); }
667
668.player-actions-wrapper { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
669.source-label-btn { 
670  background: rgba(0, 102, 204, 0.2); 
671  border: 1px solid rgba(0, 102, 204, 0.5);
672  color: #fff; 
673  padding: 10px 20px; 
674  border-radius: 999px; 
675  font-size: 13px; 
676  font-weight: 700; 
677  cursor: pointer; 
678  transition: all 0.3s var(--apple-ease);
679  letter-spacing: 0.5px;
680}
681.source-label-btn:hover { background: var(--accent); transform: scale(1.05); box-shadow: 0 4px 14px var(--accent-glow); }
682
683.refresh-btn { 
684  background: rgba(255,255,255,0.06); 
685  border: none;
686  color: white; 
687  padding: 10px; 
688  border-radius: 50%; 
689  cursor: pointer; 
690  display: flex; 
691  align-items: center; 
692  justify-content: center; 
693  transition: all 0.3s var(--apple-ease); 
694  flex-shrink: 0;
695}
696.refresh-btn:hover { background: rgba(255,255,255,0.12); transform: scale(1.1) rotate(15deg); }
697
698footer { margin-top: 80px; padding: 40px 24px; border-top: 1px solid rgba(255,255,255,0.05); text-align: center; color: var(--muted); font-size: 13px; line-height: 1.6; width: 100%; }
699footer ul { display: flex; justify-content: center; gap: 24px; list-style: none; margin-top: 12px; margin-bottom: 20px; flex-wrap: wrap; }
700footer a { color: #fff; text-decoration: none; font-weight: 600; transition: color 0.2s; }
701footer a:hover { color: var(--accent); }
702
703/* ===== RESPONSIVE MEDIA QUERIES ===== */
704@media (max-width: 1024px) {
705  .player-layout { grid-template-columns: 1fr !important; }
706  #sideStreams { display: none !important; }
707}
708
709@media (max-width: 768px) {
710  body { overflow-x: hidden !important; width: 100% !important; }
711  main { padding: 20px 16px !important; }
712  #streams { grid-template-columns: 1fr !important; gap: 20px !important; }
713
714  .header-inner { 
715    grid-template-columns: 1fr !important; 
716    padding: 12px 16px !important; 
717    gap: 12px; 
718    text-align: center;
719  }
720  .logo { justify-self: center !important; }
721  .search-container { width: 100% !important; margin-top: 0 !important; }
722  .header-actions { justify-self: center !important; width: 100%; justify-content: center; flex-wrap: wrap; }
723  
724  .hero { min-height: 35vh; padding: 40px 16px; }
725  .hero h1 { font-size: 30px !important; letter-spacing: -1px; }
726  .hero p { font-size: 14px !important; }
727
728  .player-header { 
729    flex-direction: column !important; 
730    align-items: stretch !important; 
731    gap: 14px !important; 
732    margin-bottom: 16px !important; 
733  }
734  .player-header h2 { 
735    order: -1 !important; 
736    width: 100% !important; 
737    text-align: left !important; 
738    font-size: 18px !important; 
739    margin: 0 !important; 
740  }
741
742  .player-nav-group, .player-actions-wrapper { 
743    display: flex !important; 
744    width: 100% !important; 
745    justify-content: space-between !important; 
746    gap: 10px !important; 
747  }
748  
749  .iframe-wrap { width: 100% !important; border-radius: 12px; }
750  .source-box { padding: 20px; width: 100%; }
751}
752
753@media (max-width: 480px) {
754  .header-actions { flex-direction: column; width: 100%; }
755  .discord-btn { width: 100%; justify-content: center; }
756  .player-actions-wrapper { flex-wrap: wrap; justify-content: flex-start !important; }
757}
758</style>
759</head>
760<body>
761
762<header>
763  <div class="header-inner">
764    <a href="/" class="logo">Stream<span>Portal</span></a>
765    <div class="search-container">
766      <div class="search-wrap">
767        <input id="globalSearch" type="text" placeholder="Search events, sports...">
768        <button id="clearSearch">×</button>
769      </div>
770      <div id="searchDropdown" class="search-modal-dropdown"></div>
771    </div>
772    <div class="header-actions">
773      <a href="https://discord.gg/5XNJzcVxW3" target="_blank" class="discord-btn">
774        <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994.021-.041.001-.09-.041-.106a13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.927 1.793 8.18 
7741.793 12.061 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.028zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
775        Discord
776      </a>
777      <a href="https://streamportal.pages.dev/multiview.html" class="multiview-btn">Multi-view</a>
778    </div>
779  </div>
780</header>
781
782<section class="hero" id="heroSection">
783  <div class="hero-bg"></div>
784  <div class="hero-content">
785    <div class="hero-badge">LIVE SPORTS · HD STREAMS · MULTI-VIEW</div>
786    <h1>Watch Every Big Match.<br>Instantly.</h1>
787    <p>Fast, clean and reliable live sports streaming with real-time events, multi-view support and backup streams built in.</p>
788    <div class="hero-buttons">
789      <button class="hero-btn primary" onclick="scrollToStreams()">Explore Streams</button>
790    </div>
791  </div>
792</section>
793
794<main>
795  <div id="listView">
796    <div id="tabs"></div>
797    <div id="streams">Loading Events...</div>
798  </div>
799
800  <div id="playerView">
801    <div class="player-layout">
802      <div id="sideStreams"></div>
803      <div id="player"></div>
804    </div>
805  </div>
806
807  <footer>
808    <div>STUCK ON LOADING? USE A FREE VPN HERE</div>
809    <ul>
810      <li><a href="https://chromewebstore.google.com/detail/urban-vpn-proxy/eppiocemhmnlbhjplcgkofciiegomcon" target="_blank">PC</a></li>
811      <li><a href="https://play.google.com/store/apps/details?id=com.free.vpn.super.hotspot.open" target="_blank">Android</a></li>
812      <li><a href="https://apps.apple.com/us/app/vpn-super-unlimited-proxy/id1370293473" target="_blank">Apple</a></li>
813    </ul>
814    Disclaimer: We only link to streams available on the internet. We do not host any content.
815    
816    <div style="max-width: 300px; margin: 30px auto 0 auto; width: 100%;">
817      
818    </div>
819  </footer>
820</main>
821
822<script>
823const WORKER_URL = 'https://stream.streamportal1111.workers.dev';
824const API = `${WORKER_URL}/streams`;
825const fallbackLogo = 'images/logo.jpg';
826
827let categories = [];
828let countdownInterval = null;
829let activeCategoryId = null;
830
831// Safe Firebase key helper function
832function getStreamId(stream) {
833  return (stream.name || 'stream').replace(/[^a-zA-Z0-9_-]/g, '_');
834}
835
836function scrollToStreams() {
837  const tabsEl = document.getElementById('tabs');
838  const headerHeight = document.querySelector('header').offsetHeight;
839  const topPos = tabsEl.getBoundingClientRect().top + window.pageYOffset - headerHeight - 30;
840  window.scrollTo({ top: topPos, behavior: 'smooth' });
841}
842
843async function init() {
844  try {
845    const res = await fetch(`${API}?t=${Date.now()}`, { cache: 'no-store' });
846    const data = await res.json();
847    
848    let freshCategories = data.streams || [];
849    
850    freshCategories.forEach(cat => {
851      let name = cat.category.trim();
852      if (name.toLowerCase() === 'fight') {
853        cat.category = 'Fighting';
854      }
855      if(cat.streams) {
856        cat.streams.forEach(s => s.tag = cat.category);
857      }
858    });
859
860    freshCategories.forEach(cat => {
861      if (cat.streams) {
862        cat.streams = cat.streams.filter(s => {
863          const hasSources = s.sources && Array.isArray(s.sources) && s.sources.length > 0;
864          if (!hasSources) return false;
865          
866          const hasValidLinks = s.sources.every(src => src.source && src.id);
867          return hasValidLinks && !s.noLinksAvailable;
868        });
869      }
870    });
871
872const now = Math.floor(Date.now() / 1000);
873const getSportDuration = (tag) => {
874  const t = (tag || '').toLowerCase();
875  if (t.includes('football') || t.includes('soccer')) return 10800; // 3 hours (covers 90 mins + extra time & penalties)
876  if (t.includes('basketball')) return 12600;                        // 3.5 hours (handles multiple OTs)
877  if (t.includes('hockey') || t.includes('rugby')) return 12600;     // 3.5 hours (handles OTs)
878  if (t.includes('american football')) return 14400;                 // 4 hours 
879  if (t.includes('baseball')) return 14400;                          // 4 hours 
880  if (t.includes('fight')) return 21600;                             // 6 hours (full cards)
881  if (t.includes('cricket')) return 28800;                           // 8 hours 
882  return 14400; // Default 4 hours
883};
884
885    freshCategories.forEach(cat => {
886      if (cat.streams) {
887        cat.streams = cat.streams.filter(s => {
888          const start = parseInt(s.starts_at) || 0;
889          let end = parseInt(s.ends_at) || 0;
890          const alwaysLive = s.always_live == '1';
891
892          if (end === 0 && start > 0) {
893            end = start + getSportDuration(s.tag);
894          }
895
896          if (!alwaysLive && end > 0 && now >= end) {
897            return false; 
898          }
899          return true;
900        });
901      }
902    });
903
904    freshCategories = freshCategories.filter(cat => cat.streams && cat.streams.length > 0);
905
906    freshCategories.sort((a, b) => {
907      const aIsFootball = a.category.toLowerCase().trim() === 'football';
908      const bIsFootball = b.category.toLowerCase().trim() === 'football';
909      if (aIsFootball && !bIsFootball) return -1;
910      if (!aIsFootball && bIsFootball) return 1;
911      return a.category.localeCompare(b.category);
912    });
913
914    categories = freshCategories;
915    renderTabs();
916    
917    if (activeCategoryId && categories.some(c => c.id == activeCategoryId)) {
918      selectCategory(activeCategoryId);
919    } else if (categories.length > 0) {
920      const footballCat = categories.find(c => c.category.toLowerCase().trim() === 'football');
921      selectCategory(footballCat ? footballCat.id : categories[0].id);
922    } else {
923      document.getElementById('streams').innerHTML = '<div style="padding: 40px; color: var(--muted); text-align: center; width: 100%; font-size: 16px;">No live streams available right now.</div>';
924    }
925  } catch (err) {
926    console.error("Sync error:", err);
927    document.getElementById('streams').innerHTML = `
928      <div style="padding: 40px; color: var(--muted); text-align: center; width: 100%; font-size: 16px; display: flex; flex-direction: column; align-items: center; gap: 16px; grid-column: 1/-1;">
929        <span>Failed to connect to stream server.</span>
930        <button onclick="init()" style="padding: 10px 24px; border-radius: 999px; background: var(--accent); color: #fff; border: none; font-weight: 600; cursor: pointer; box-shadow: 0 4px 14px var(--accent-glow);">Refresh</button>
931      </div>
932    `;
933  }
934}
935
936function renderTabs() {
937  const container = document.getElementById('tabs');
938  container.innerHTML = '';
939  categories.forEach(cat => {
940    const div = document.createElement('div');
941    div.className = 'tab';
942    div.textContent = cat.category;
943    div.dataset.id = cat.id;
944    div.onclick = () => selectCategory(cat.id);
945    container.appendChild(div);
946  });
947}
948
949function selectCategory(id) {
950  activeCategoryId = id;
951  document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.id == id));
952  const cat = categories.find(c => c.id == id);
953  if(cat && cat.streams) {
954    renderGrid(cat.streams);
955  }
956}
957
958function renderGrid(streams) {
959  const container = document.getElementById('streams');
960  container.innerHTML = '';
961  
962  if (!streams || streams.length === 0) {
963    container.innerHTML = '<div style="padding: 40px; color: var(--muted); text-align: center; width: 100%; grid-column: 1/-1; font-size: 16px;">No streams available right now.</div>';
964    return;
965  }
966
967const now = Math.floor(Date.now() / 1000);
968const getSportDuration = (tag) => {
969  const t = (tag || '').toLowerCase();
970  if (t.includes('football') || t.includes('soccer')) return 10800; // 3 hours (covers 90 mins + extra time & penalties)
971  if (t.includes('basketball')) return 12600;                        // 3.5 hours (handles multiple OTs)
972  if (t.includes('hockey') || t.includes('rugby')) return 12600;     // 3.5 hours (handles OTs)
973  if (t.includes('american football')) return 14400;                 // 4 hours 
974  if (t.includes('baseball')) return 14400;                          // 4 hours 
975  if (t.includes('fight')) return 21600;                             // 6 hours (full cards)
976  if (t.includes('cricket')) return 28800;                           // 8 hours 
977  return 14400; // Default 4 hours
978};
979
980  const processedStreams = [];
981
982  streams.forEach(s => {
983    const start = parseInt(s.starts_at) || 0;
984    let end = parseInt(s.ends_at) || 0;
985    const alwaysLive = s.always_live == '1';
986
987    if (end === 0 && start > 0) {
988      end = start + getSportDuration(s.tag);
989    }
990
991    if (!alwaysLive && end > 0 && now >= end) {
992      return; 
993    }
994
995    const isLive = alwaysLive || (start <= now && (end === 0 || now < end));
996    const isUpcoming = start > now;
997
998    let weight = 2;
999    if (isLive) weight = 0;
1000    else if (isUpcoming) weight = 1;
1001
1002    if (weight !== 2) {
1003      processedStreams.push({ 
1004        ...s, 
1005        _sortWeight: weight, 
1006        _computedStart: start,
1007        _computedEnd: end,
1008        _isLive: isLive
1009      });
1010    }
1011  });
1012
1013  if (processedStreams.length === 0) {
1014    container.innerHTML = '<div style="padding: 40px; color: var(--muted); text-align: center; width: 100%; grid-column: 1/-1; font-size: 16px;">No active or upcoming streams available.</div>';
1015    return;
1016  }
1017
1018  processedStreams.sort((a, b) => {
1019    if (a._sortWeight !== b._sortWeight) {
1020      return a._sortWeight - b._sortWeight;
1021    }
1022    return a._computedStart - b._computedStart;
1023  });
1024
1025  processedStreams.forEach(s => {
1026    const posterSrc = s.poster ? `${WORKER_URL}/image?url=${encodeURIComponent(s.poster)}` : fallbackLogo;
1027    const streamId = getStreamId(s);
1028    const div = document.createElement('div');
1029    div.className = 'stream';
1030    
1031    div.innerHTML = `
1032      <div class="poster-wrapper">
1033        ${s._isLive ? `<div class="stream-live-badge"><span></span>LIVE</div>` : ''}
1034        <div class="card-viewers-badge viewer-badge" style="display: none;">
1035          <span class="dot"></span>
1036          <span class="stream-viewer-count" data-stream-id="${streamId}">0</span>
1037        </div>
1038        <img class="poster" src="${posterSrc}" onerror="this.src='${fallbackLogo}';" loading="lazy">
1039      </div>
1040      <div class="info">
1041        <strong>${s.name}</strong>
1042        <div class="meta">
1043          <span>${s.tag || ''}</span>
1044          <div class="countdown" data-start="${s.starts_at || 0}" data-end="${s.ends_at || 0}" data-always="${s.always_live || 0}"></div>
1045        </div>
1046      </div>
1047    `;
1048    div.onclick = () => openTheater(s);
1049    container.appendChild(div);
1050  });
1051  
1052  if (window.refreshViewerBadges) window.refreshViewerBadges();
1053  updateTimers();
1054}
1055
1056function getFilteredSidebarStreams(currentStreamName) {
1057  const now = Math.floor(Date.now() / 1000);
1058  const DAY = 86400; 
1059  const exclude = ["Rally TV", "Willow Cricket", "Tennis Channel", "Fox Cricket", "Fox League", "ESPN"];
1060  
1061  const allStreams = categories.flatMap(c => c.streams || []).filter(s => 
1062    s.name !== currentStreamName && !exclude.includes(s.name) && s.sources && s.sources.length > 0 && !s.noLinksAvailable
1063  );
1064  
1065  const live = [];
1066  const upcoming = [];
1067
1068  for (const s of allStreams) {
1069    const start = parseInt(s.starts_at) || 0;
1070    const end = parseInt(s.ends_at) || 0;
1071    const always = s.always_live == '1';
1072
1073    const isLive = always || (start <= now && (!end || now < end));
1074    const isUpcoming = start && start > now && (start - now <= DAY);
1075
1076    if (isLive) live.push(s);
1077    else if (isUpcoming) upcoming.push(s);
1078  }
1079
1080  live.sort((a, b) => (parseInt(a.starts_at) || 0) - (parseInt(b.starts_at) || 0));
1081  upcoming.sort((a, b) => (parseInt(a.starts_at) || 0) - (parseInt(b.starts_at) || 0));
1082
1083  return [...live, ...upcoming].slice(0, 20);
1084}
1085
1086async function openTheater(stream) {
1087  document.getElementById('heroSection').style.display = 'none';
1088  document.getElementById('listView').style.display = 'none';
1089  
1090  const pv = document.getElementById('playerView');
1091  pv.classList.add('active');
1092  window.scrollTo({ top: 0, behavior: 'smooth' });
1093
1094  const side = document.getElementById('sideStreams');
1095  side.innerHTML = `
1096    <div class="now-card">
1097      <div class="now-label">NOW WATCHING</div>
1098      <div class="now-title">${stream.name}</div>
1099      <div class="now-meta">${stream.tag || ''}</div>
1100    </div>
1101  `;
1102
1103  const targetedStreams = getFilteredSidebarStreams(stream.name);
1104  targetedStreams.forEach(s => {
1105    const sidePosterSrc = s.poster ? `${WORKER_URL}/image?url=${encodeURIComponent(s.poster)}` : fallbackLogo;
1106    const item = document.createElement('div');
1107    item.className = 'stream';
1108    item.innerHTML = `
1109      <div class="poster-wrapper">
1110        <img class="poster" src="${sidePosterSrc}" onerror="this.src='${fallbackLogo}';" loading="lazy">
1111      </div>
1112      <div class="info">
1113        <strong>${s.name}</strong>
1114        <div class="meta">
1115          <span>${s.tag || ''}</span>
1116          <div class="countdown" data-start="${s.starts_at || 0}" data-end="${s.ends_at || 0}" data-always="${s.always_live || 0}">LIVE</div>
1117        </div>
1118      </div>
1119    `;
1120    item.onclick = () => openTheater(s);
1121    side.appendChild(item);
1122  });
1123
1124  const player = document.getElementById('player');
1125  player.innerHTML = `
1126    <div class="player-header">
1127      <div class="player-nav-group">
1128        <button class="back-btn" id="backToGridBtn">
1129          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
1130          Back
1131        </button>
1132        <h2>${stream.name}</h2>
1133      </div>
1134      <div class="player-actions-wrapper">
1135        <div class="theater-viewers-pill">
1136          <span class="dot"></span>
1137          <span id="theaterViewerCountValue">1</span> Watching
1138        </div>
1139        <button class="source-label-btn" id="sourceToggleBtn" style="opacity: 0.5; pointer-events: none;">LOADING...</button>
1140        <button class="refresh-btn" id="nextSourceBtn" title="Skip to Next Backup" style="display: none;">
1141          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 4 15 12 5 20 5 4"></polygon><line x1="19" y1="5" x2="19" y2="19"></line></svg>
1142        </button>
1143        <button class="refresh-btn" id="streamRefreshBtn" title="Refresh Feed Source">
1144          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin='round'><path d='M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67'></path></svg>
1145        </button>
1146      </div>
1147    </div>
1148    <div class="iframe-wrap"><div class="stream-loader"><div class="spinner"></div></div></div>
1149  `;
1150
1151  // Track viewer presence entry for this stream
1152  const streamId = getStreamId(stream);
1153  if (window.enterStreamTracker) window.enterStreamTracker(streamId);
1154
1155  const wrap = player.querySelector('.iframe-wrap');
1156  let sources = [];
1157  let currentIndex = 0;
1158  let animationFrameId = null;
1159
1160  const loadSource = (idx) => {
1161    if (!sources[idx]) return;
1162    currentIndex = idx;
1163    
1164    if (animationFrameId) {
1165      cancelAnimationFrame(animationFrameId);
1166      animationFrameId = null;
1167    }
1168
1169    wrap.innerHTML = '<div class="stream-loader"><div class="spinner"></div></div>';
1170    
1171    if (sources[idx].isPlaceholder || sources[idx].isFinishedPlaceholder) {
1172      const isFinished = sources[idx].isFinishedPlaceholder;
1173      
1174      wrap.innerHTML = `
1175        <div style="position: absolute; inset: 0; background: #07090e; display: flex; flex-direction: column; align-items: center; justify-content: center; overflow: hidden; border-radius: 16px;">
1176          <canvas id="movingGateCanvas" style="position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; opacity: 0.5;"></canvas>
1177          
1178          <div style="position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 30px 20px; text-align: center; background: rgba(20, 23, 32, 0.75); backdrop-filter: saturate(150%) blur(30px); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 24px; box-shadow: 0 30px 60px rgba(0,0,0,0.8); max-width: 90%;">
1179            <div style="position: relative; width: 64px; height: 64px; margin-bottom: 24px; display: flex; align-items: center; justify-content: center;">
1180              <div style="position: absolute; inset: 0; border: 2px dashed ${isFinished ? '#ff3b30' : 'var(--accent)'}; border-radius: 50%; animation: spin 20s linear infinite; opacity: 0.4;"></div>
1181              <div style="position: absolute; inset: 6px; border: 1px solid ${isFinished ? 'rgba(255, 59, 48, 0.3)' : 'var(--accent-glow)'}; border-radius: 50%; background: ${isFinished ? 'rgba(255, 59, 48, 0.1)' : 'rgba(0, 102, 204, 0.1)'};"></div>
1182              ${isFinished ? 
1183                `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ff3b30" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="position: relative; z-index: 3;"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>` :
1184                `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="position: relative; z-index: 3;"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`
1185              }
1186            </div>
1187            
1188            <h3 style="font-size: clamp(18px, 2.5vw, 22px); font-weight: 800; letter-spacing: -0.5px; margin-bottom: 8px; color: #fff;">
1189              ${isFinished ? 'EVENT CONCLUDED' : 'STREAM STARTING SOON'}
1190            </h3>
1191            <p style="color: var(--muted); font-size: 14.5px; max-width: 360px; margin-bottom: 28px; line-height: 1.5;">
1192              ${isFinished 
1193                ? 'This match has ended. Check out our other live events.' 
1194                : ((parseInt(stream.starts_at) || 0) - Math.floor(Date.now() / 1000) <= 900)
1195                  ? 'We are waiting for the broadcaster. Tap refresh to check if the stream has started.'
1196                  : 'Streams for this event are not yet available. Please check back about 15 minutes before kick-off.'
1197              }
1198            </p>
1199            
1200            ${isFinished ? 
1201              `<button onclick="document.getElementById('backToGridBtn').click()" style="font-size: 14px; font-weight: 700; padding: 12px 28px; background: var(--accent); color: #fff; border: none; border-radius: 999px; cursor: pointer; transition: all 0.3s; transform-origin: center; box-shadow: 0 4px 14px var(--accent-glow);">Return to Dashboard</button>` :
1202              `<div class="countdown" data-start="${stream.starts_at || 0}" data-end="${stream.ends_at || 0}" data-always="${stream.always_live || 0}" style="font-size: 16px; font-weight: 700; font-family: 'Inter', monospace; letter-spacing: 0.5px; padding: 10px 24px; background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.15); color: #fff; border-radius: 12px;">00H 00M 00S</div>`
1203            }
1204          </div>
1205        </div>
1206      `;
1207
1208      setTimeout(() => {
1209        const canvas = document.getElementById('movingGateCanvas');
1210        if (!canvas) return;
1211        const ctx = canvas.getContext('2d');
1212        
1213        const resize = () => {
1214          canvas.width = canvas.offsetWidth * window.devicePixelRatio;
1215          canvas.height = canvas.offsetHeight * window.devicePixelRatio;
1216          ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
1217        };
1218        resize();
1219
1220        const particles = [];
1221        for (let i = 0; i < 40; i++) {
1222          particles.push({
1223            x: Math.random() * (canvas.width / window.devicePixelRatio),
1224            y: Math.random() * (canvas.height / window.devicePixelRatio),
1225            size: Math.random() * 2 + 0.5,
1226            speedY: -(Math.random() * 0.3 + 0.1),
1227            alpha: Math.random() * 0.3 + 0.1
1228          });
1229        }
1230
1231        let wavePhase = 0;
1232        const renderLoop = () => {
1233          if (!document.getElementById('movingGateCanvas')) return;
1234          const w = canvas.width / window.devicePixelRatio;
1235          const h = canvas.height / window.devicePixelRatio;
1236          ctx.clearRect(0, 0, w, h);
1237          
1238          particles.forEach(p => {
1239            p.y += p.speedY;
1240            if (p.y < -10) p.y = h + 10;
1241            ctx.fillStyle = isFinished ? `rgba(255, 59, 48, ${p.alpha})` : `rgba(0, 102, 204, ${p.alpha})`;
1242            ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill();
1243          });
1244
1245          wavePhase += 0.005;
1246          ctx.fillStyle = isFinished ? 'rgba(255, 59, 48, 0.02)' : 'rgba(0, 102, 204, 0.03)';
1247          ctx.beginPath(); ctx.moveTo(0, h);
1248          for (let i = 0; i <= w; i += 10) {
1249            const y = h - 40 + Math.sin(i * 0.006 + wavePhase) * 20;
1250            ctx.lineTo(i, y);
1251          }
1252          ctx.lineTo(w, h); ctx.fill();
1253
1254          animationFrameId = requestAnimationFrame(renderLoop);
1255        };
1256        renderLoop();
1257      }, 50);
1258
1259      updateTimers();
1260      return;
1261    }
1262
1263    const iframe = document.createElement('iframe');
1264    iframe.src = sources[idx].url;
1265    iframe.allowFullscreen = true;
1266    iframe.onload = () => {
1267      const loader = wrap.querySelector('.stream-loader');
1268      if (loader) loader.remove();
1269    };
1270    wrap.appendChild(iframe);
1271    attachGate(wrap);
1272  };
1273
1274  if (stream.sources && stream.sources.length > 0) {
1275    try {
1276      const resolvePromises = stream.sources.map(src => 
1277        fetch(`${WORKER_URL}/resolve-stream?source=${src.source}&id=${src.id}`).then(r => {
1278          if (!r.ok) return []; 
1279          return r.json();
1280        }).catch(() => []) 
1281      );
1282      
1283      const results = await Promise.all(resolvePromises);
1284      results.forEach((streamList) => {
1285        if (Array.isArray(streamList)) {
1286          streamList.forEach((item) => {
1287            if (item.embedUrl) {
1288              const qualityLabel = item.hd ? 'HD' : 'SD';
1289              const langLabel = item.language ? item.language.toUpperCase() : 'ENG';
1290              sources.push({
1291                url: item.embedUrl,
1292                label: `${langLabel} (${qualityLabel})`
1293              });
1294            }
1295          });
1296        }
1297      });
1298    } catch(err) {
1299      console.error("Error parsing backend stream links:", err);
1300    }
1301  }
1302
1303  const now = Math.floor(Date.now() / 1000);
1304  const startTime = parseInt(stream.starts_at) || 0;
1305  let endTime = parseInt(stream.ends_at) || 0;
1306
1307  if (endTime === 0 && startTime > 0) {
1308    endTime = startTime + 14400; 
1309  }
1310
1311  if (sources.length === 0) {
1312    stream.noLinksAvailable = true;
1313    if (now >= endTime) {
1314      sources.push({ url: "", label: "Match Finished", isFinishedPlaceholder: true });
1315    } else {
1316      sources.push({ url: "", label: "Stream Awaiting Start", isPlaceholder: true });
1317    }
1318  }
1319
1320  const toggleBtn = document.getElementById('sourceToggleBtn');
1321  if (sources.length > 0 && !sources[0].isPlaceholder && !sources[0].isFinishedPlaceholder) {
1322    toggleBtn.style.opacity = "1";
1323    toggleBtn.style.pointerEvents = "auto";
1324    toggleBtn.textContent = "SOURCES";
1325    loadSource(0);
1326  } else {
1327    const isFinished = now >= endTime;
1328    toggleBtn.style.opacity = "0.5";
1329    toggleBtn.style.pointerEvents = "none";
1330    toggleBtn.textContent = isFinished ? "OFFLINE" : "AWAITING FEEDS";
1331    loadSource(0);
1332  }
1333
1334  const nextBtn = document.getElementById('nextSourceBtn');
1335  if (sources.length > 1) {
1336    nextBtn.style.display = 'flex';
1337  }
1338
1339  nextBtn.onclick = () => {
1340    if (sources.length > 1) {
1341      let nextIdx = (currentIndex + 1) % sources.length;
1342      loadSource(nextIdx);
1343    }
1344  };
1345
1346  document.getElementById('backToGridBtn').onclick = () => {
1347    if (window.exitStreamTracker) window.exitStreamTracker();
1348    if (animationFrameId) cancelAnimationFrame(animationFrameId);
1349    pv.classList.remove('active');
1350    document.getElementById('heroSection').style.display = 'flex';
1351    document.getElementById('listView').style.display = 'block';
1352    player.innerHTML = '';
1353    document.getElementById('globalSearch').value = '';
1354    document.getElementById('clearSearch').style.opacity = '0';
1355    document.getElementById('searchDropdown').classList.remove('active');
1356    selectCategory(activeCategoryId);
1357  };
1358
1359  document.getElementById('streamRefreshBtn').onclick = () => {
1360    const currentNow = Math.floor(Date.now() / 1000);
1361    if (sources[0]?.isPlaceholder && (parseInt(stream.starts_at) || 0) <= currentNow) {
1362      if (animationFrameId) cancelAnimationFrame(animationFrameId);
1363      openTheater(stream);
1364    } else {
1365      loadSource(currentIndex);
1366    }
1367  };
1368  
1369  toggleBtn.onclick = () => {
1370    if (sources.length === 0 || sources[0].isPlaceholder || sources[0].isFinishedPlaceholder) return;
1371    
1372    const modal = document.createElement('div');
1373    modal.className = 'source-modal';
1374    const box = document.createElement('div');
1375    box.className = 'source-box';
1376    box.style.cssText = "display:flex; flex-direction:column; max-height:75vh; height:460px; overflow:hidden; padding:24px; position:relative;";
1377    
1378    const grouped = {};
1379    sources.forEach((src, originalIdx) => {
1380      let label = src.label.toUpperCase();
1381      let lang = 'ENGLISH';
1382
1383      if (/\b(SPANISH|ESP|SPA|ES)\b/.test(label)) lang = 'SPANISH';
1384      else if (/\b(FRENCH|FRA|FRE|FR)\b/.test(label)) lang = 'FRENCH';
1385      else if (/\b(PORTUGUESE|POR|PT)\b/.test(label)) lang = 'PORTUGUESE';
1386      else if (/\b(ITALIAN|ITA|IT)\b/.test(label)) lang = 'ITALIAN';
1387      else if (/\b(GERMAN|GER|DEU|DE)\b/.test(label)) lang = 'GERMAN';
1388      else if (/\b(ARABIC|ARA|AR)\b/.test(label)) lang = 'ARABIC';
1389      else if (/\b(SWEDISH|SWE|SV|SE)\b/.test(label)) lang = 'SWEDISH';
1390      else if (/\b(DUTCH|NLD|DUT|NL)\b/.test(label)) lang = 'DUTCH';
1391      else if (/\b(POLISH|POL|PL)\b/.test(label)) lang = 'POLISH';
1392      else if (/\b(RUSSIAN|RUS|RU)\b/.test(label)) lang = 'RUSSIAN';
1393      else if (/\b(TURKISH|TUR|TR)\b/.test(label)) lang = 'TURKISH';
1394      else if (/\b(GREEK|GRE|ELL|EL|GR)\b/.test(label)) lang = 'GREEK';
1395      else if (/\b(DANISH|DAN|DA|DK)\b/.test(label)) lang = 'DANISH';
1396      else if (/\b(NORWEGIAN|NOR|NO)\b/.test(label)) lang = 'NORWEGIAN';
1397      else if (/\b(FINNISH|FIN|FI)\b/.test(label)) lang = 'FINNISH';
1398      else if (/\b(JAPANESE|JPN|JA|JP)\b/.test(label)) lang = 'JAPANESE';
1399      else if (/\b(KOREAN|KOR|KO|KR)\b/.test(label)) lang = 'KOREAN';
1400      else if (/\b(CHINESE|CHI|ZHO|Mandarin|Cantonese|CN|ZH)\b/.test(label)) lang = 'CHINESE';
1401      else if (/\b(HINDI|HIN|HI)\b/.test(label)) lang = 'HINDI';
1402      else if (/\b(ROMANIAN|RON|ROM|RO)\b/.test(label)) lang = 'ROMANIAN';
1403      else if (/\b(HUNGARIAN|HUN|HU)\b/.test(label)) lang = 'HUNGARIAN';
1404      else if (/\b(CZECH|CES|CZE|CS|CZ)\b/.test(label)) lang = 'CZECH';
1405      else if (/\b(UKRAINIAN|UKR|UA)\b/.test(label)) lang = 'UKRAINIAN';
1406      else if (/\b(VIETNAMESE|VIE|VN)\b/.test(label)) lang = 'VIETNAMESE';
1407      
1408      else if (/[-_](ES|FR|PT|IT|DE|AR|SE|NL|PL|RU|TR|GR|DK|NO|FI|JP|KR|CN|HI|RO|HU|CZ|UA|VN)(\b|_)/i.test(label)) {}
1409
1410      if (!grouped[lang]) grouped[lang] = [];
1411      grouped[lang].push({ src, originalIdx });
1412    });
1413
1414    const languages = Object.keys(grouped).sort((a, b) => a === 'ENGLISH' ? -1 : b === 'ENGLISH' ? 1 : a.localeCompare(b));
1415    let activeLang = languages[0];
1416
1417    const title = document.createElement('h3');
1418    title.textContent = 'Select Stream Source';
1419    box.appendChild(title);
1420
1421    const tabsContainer = document.createElement('div');
1422    tabsContainer.style.cssText = "display:flex; gap:10px; margin-bottom:20px; overflow-x:auto; white-space:nowrap; padding-bottom:6px; flex-shrink:0; scrollbar-width:none;";
1423    
1424    const optionsContainer = document.createElement('div');
1425    optionsContainer.style.cssText = "display:flex; flex-direction:column; gap:10px; overflow-y:auto; flex-grow:1; padding-right:6px;";
1426
1427    const renderLanguageOptions = (lang) => {
1428      optionsContainer.innerHTML = '';
1429      grouped[lang].forEach(({ src, originalIdx }) => {
1430        const b = document.createElement('button');
1431        b.className = `source-option ${originalIdx === currentIndex ? 'active' : ''}`;
1432        b.textContent = src.label;
1433        b.onclick = () => { loadSource(originalIdx); modal.remove(); };
1434        optionsContainer.appendChild(b);
1435      });
1436    };
1437
1438    languages.forEach((lang) => {
1439      const tabBtn = document.createElement('button');
1440      tabBtn.textContent = lang;
1441      tabBtn.style.cssText = `flex: 0 0 auto; padding: 8px 16px; border-radius: 999px; font-size: 13px; font-weight: 700; cursor: pointer; white-space: nowrap; border: none; background: ${lang === activeLang ? 'var(--accent)' : 'rgba(255, 255, 255, 0.06)'}; color: #fff; transition: all 0.3s; box-shadow: ${lang === activeLang ? '0 4px 14px var(--accent-glow)' : 'none'};`;
1442      
1443      tabBtn.onclick = () => {
1444        activeLang = lang;
1445        Array.from(tabsContainer.children).forEach(btn => {
1446           const isActive = (btn.textContent === lang);
1447           btn.style.background = isActive ? 'var(--accent)' : 'rgba(255, 255, 255, 0.06)';
1448           btn.style.boxShadow = isActive ? '0 4px 14px var(--accent-glow)' : 'none';
1449        });
1450        renderLanguageOptions(lang);
1451      };
1452      tabsContainer.appendChild(tabBtn);
1453    });
1454
1455    box.appendChild(tabsContainer);
1456    box.appendChild(optionsContainer);
1457    renderLanguageOptions(activeLang);
1458
1459    modal.appendChild(box);
1460    document.body.appendChild(modal);
1461    modal.onclick = e => { if (e.target === modal) modal.remove(); };
1462  };
1463  updateTimers();
1464}
1465
1466function attachGate(wrap) {
1467  wrap.querySelectorAll('.stream-gate').forEach(g => g.remove());
1468  const iframe = wrap.querySelector('iframe');
1469  if (!iframe) return;
1470
1471  iframe.style.pointerEvents = "none";
1472  const links = ["https://hai8g.com/4/11608660", "https://hai8g.com/4/11610289"];
1473  let clicks = 0;
1474
1475  const gate = document.createElement('div');
1476  Object.assign(gate.style, {
1477    position: 'absolute', inset: '0', zIndex: '999', background: 'transparent', cursor: 'pointer'
1478  });
1479
1480  gate.onclick = (e) => {
1481    e.preventDefault(); e.stopPropagation();
1482    window.open(links[clicks], '_blank');
1483    clicks++;
1484    if(clicks >= 2) {
1485      gate.remove();
1486      iframe.style.pointerEvents = "auto";
1487    }
1488  };
1489  wrap.appendChild(gate);
1490}
1491
1492function updateTimers() {
1493  const now = Math.floor(Date.now() / 1000);
1494  document.querySelectorAll('.countdown').forEach(el => {
1495    const start = parseInt(el.dataset.start), end = parseInt(el.dataset.end), always = el.dataset.always == '1';
1496    if(always || (start <= now && (!end || now < end))) {
1497      el.textContent = "LIVE"; el.className = 'countdown live';
1498    } else if (start > now) {
1499      const diff = start - now;
1500      const m = Math.floor(diff / 60) % 60, h = Math.floor(diff / 3600);
1501      el.textContent = `${h}h ${m}m`; el.className = 'countdown upcoming';
1502    } else {
1503      el.textContent = "LIVE"; el.className = 'countdown live';
1504    }
1505  });
1506}
1507
1508const searchInput = document.getElementById('globalSearch');
1509const clearSearch = document.getElementById('clearSearch');
1510const searchDropdown = document.getElementById('searchDropdown');
1511
1512searchInput.addEventListener('input', () => {
1513  const val = searchInput.value.toLowerCase().trim();
1514  if(!val) {
1515    clearSearch.style.opacity = '0';
1516    clearSearch.style.pointerEvents = 'none';
1517    searchDropdown.classList.remove('active');
1518    if (document.getElementById('playerView').classList.contains('active') === false) {
1519      selectCategory(activeCategoryId);
1520    }
1521    return;
1522  }
1523  clearSearch.style.opacity = '1';
1524  clearSearch.style.pointerEvents = 'auto';
1525
1526  const dataset = categories.flatMap(c => c.streams || []).filter(s => s.sources && s.sources.length > 0 && !s.noLinksAvailable);
1527  const matching = dataset.filter(s => s.name.toLowerCase().includes(val) || (s.tag && s.tag.toLowerCase().includes(val)));
1528  
1529  if (document.getElementById('playerView').classList.contains('active')) {
1530    searchDropdown.innerHTML = '';
1531    if(!matching.length) {
1532      searchDropdown.innerHTML = '<div style="padding: 16px; font-size: 14px; color: var(--muted); text-align: center;">No matches found</div>';
1533    } else {
1534      matching.slice(0, 10).forEach(s => {
1535        const item = document.createElement('div');
1536        item.className = 'search-modal-item';
1537        const imgUrl = s.poster ? `${WORKER_URL}/image?url=${encodeURIComponent(s.poster)}` : fallbackLogo;
1538        item.innerHTML = `
1539          <img src="${imgUrl}" onerror="this.src='${fallbackLogo}';">
1540          <div>
1541            <div class="title">${s.name}</div>
1542            <div class="tag">${s.tag || ''}</div>
1543          </div>
1544        `;
1545        item.onclick = () => {
1546          openTheater(s);
1547          searchInput.value = '';
1548          clearSearch.style.opacity = '0';
1549          searchDropdown.classList.remove('active');
1550        };
1551        searchDropdown.appendChild(item);
1552      });
1553    }
1554    searchDropdown.classList.add('active');
1555  } else {
1556    searchDropdown.classList.remove('active');
1557    renderGrid(matching);
1558  }
1559});
1560
1561clearSearch.onclick = () => {
1562  searchInput.value = '';
1563  clearSearch.style.opacity = '0';
1564  clearSearch.style.pointerEvents = 'none';
1565  searchDropdown.classList.remove('active');
1566  if (document.getElementById('playerView').classList.contains('active') === false) {
1567    selectCategory(activeCategoryId);
1568  }
1569};
1570
1571document.addEventListener('click', (e) => {
1572  if (!searchDropdown.contains(e.target) && e.target !== searchInput) {
1573    searchDropdown.classList.remove('active');
1574  }
1575});
1576
1577if (!countdownInterval) countdownInterval = setInterval(updateTimers, 30000);
1578setInterval(init, 60000);
1579init();
1580</script>
1580
1581
1582<!-- Optimized Firebase Presence Script -->
1583<script type="module">
1584  import { initializeApp } from "https://www.gstatic.com/firebasejs/10.8.1/firebase-app.js";
1585  import { getDatabase, ref, onDisconnect, set, push, remove, get } from "https://www.gstatic.com/firebasejs/10.8.1/firebase-database.js";
1586
1587  const firebaseConfig = {
1588    apiKey: "AIzaSyDWSAaJ4SLuXxTcn6ArxhqSMSzI9dV5vCU",
1589    authDomain: "viewers-9f704.firebaseapp.com",
1590    databaseURL: "https://viewers-9f704-default-rtdb.firebaseio.com",
1591    projectId: "viewers-9f704",
1592    storageBucket: "viewers-9f704.firebasestorage.app",
1593    messagingSenderId: "502411700095",
1594    appId: "1:502411700095:web:bc94c92f72f7bc25f55c4a"
1595  };
1596
1597  const app = initializeApp(firebaseConfig);
1598  const db = getDatabase(app);
1599
1600  let currentConnRef = null;
1601  let currentStreamId = null;
1602
1603  window.enterStreamTracker = function(streamId) {
1604    window.exitStreamTracker();
1605    currentStreamId = streamId;
1606    
1607    const trackingRef = push(ref(db, `stream_presence/${streamId}`));
1608    currentConnRef = trackingRef;
1609
1610    set(trackingRef, true).then(() => {
1611      onDisconnect(trackingRef).remove();
1612    });
1613
1614    // Instantly update count for the opened stream
1615    fetchSingleStreamCount(streamId);
1616  };
1617
1618  window.exitStreamTracker = function() {
1619    if (currentConnRef) {
1620      onDisconnect(currentConnRef).cancel().catch(() => {});
1621      remove(currentConnRef).catch(() => {});
1622      currentConnRef = null;
1623      currentStreamId = null;
1624    }
1625  };
1626
1627  window.addEventListener('beforeunload', () => {
1628    window.exitStreamTracker();
1629  });
1630
1631  // Pull counts periodically using get() instead of an expensive full-tree real-time socket
1632  async function fetchAllCounts() {
1633  try {
1634    // Only fetch viewer data for streams currently displayed
1635    const streamIds = [...new Set(
1636      [...document.querySelectorAll('.stream-viewer-count')]
1637        .map(el => el.dataset.streamId)
1638        .filter(Boolean)
1639    )];
1640
1641    if (!streamIds.length) return;
1642
1643    const results = await Promise.all(
1644      streamIds.map(async (streamId) => {
1645        try {
1646          const snapshot = await get(
1647            ref(db, `stream_presence/${streamId}`)
1648          );
1649
1650          const count = snapshot.exists()
1651            ? Object.keys(snapshot.val() || {}).length
1652            : 0;
1653
1654          return [streamId, count];
1655        } catch {
1656          return [streamId, 0];
1657        }
1658      })
1659    );
1660
1661    const counts = Object.fromEntries(results);
1662
1663    updateUI(counts);
1664
1665  } catch (err) {
1666    console.error("Firebase fetch error:", err);
1667  }
1668}
1669
1670  async function fetchSingleStreamCount(streamId) {
1671    try {
1672      const snapshot = await get(ref(db, `stream_presence/${streamId}`));
1673      const count = snapshot.exists() ? Object.keys(snapshot.val() || {}).length : 1;
1674      const theaterCountEl = document.getElementById('theaterViewerCountValue');
1675      if (theaterCountEl) {
1676        theaterCountEl.textContent = count;
1677      }
1678    } catch (err) {}
1679  }
1680
1681  function updateUI(counts) {
1682    document.querySelectorAll('.stream-viewer-count').forEach(el => {
1683      const sid = el.dataset.streamId;
1684      const count = counts[sid] || 0;
1685      el.textContent = count;
1686      const badge = el.closest('.viewer-badge');
1687      if (badge) {
1688        badge.style.display = count > 0 ? 'inline-flex' : 'none';
1689      }
1690    });
1691
1692    if (currentStreamId) {
1693      const theaterCountEl = document.getElementById('theaterViewerCountValue');
1694      if (theaterCountEl) {
1695        theaterCountEl.textContent = counts[currentStreamId] || 1;
1696      }
1697    }
1698  }
1699
1700  window.refreshViewerBadges = function() {
1701    fetchAllCounts();
1702  };
1703
1704  // Initial fetch on page load, then poll every 45 seconds to drastically reduce bandwidth
1705  fetchAllCounts();
1706  setInterval(fetchAllCounts, 120000);
1707</script>
1707
1708
1709<script id="cid0020000432197706336" data-cfasync="false" async src="//st.chatango.com/js/gz/emb.js" style="width: 200px;height: 324px;">{"handle":"streamportal1","arch":"js","styles":{"a":"255fd8","b":100,"c":"FFFFFF","d":"FFFFFF","e":"255fd8","g":"ffffff","k":"ffffff","l":"255fd8","m":"000066","n":"FFFFFF","p":"10","q":"255fd8","r":100,"t":0,"ab":false,"usricon":0,"pos":"bl","cv":1,"cvfntw":"bold","cvbg":"255fd8","cvw":180,"cvh":30,"surl":0,"allowpm":0,"cnrs":"1"}}</script>
1709
1710
1711</body>
1712</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.