/ index.html
index.html
  1  <!DOCTYPE html>
  2  <html lang="en">
  3  <head>
  4  <meta charset="UTF-8">
  5  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  6  <title>Anton Amapiano Mix</title>
  7  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='80' font-size='80'%3E%F0%9F%8E%B6%3C/text%3E%3C/svg%3E" />
  8  <style>
  9    @import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Outfit:wght@300;400;500&display=swap');
 10  
 11    * { margin: 0; padding: 0; box-sizing: border-box; }
 12  
 13    body {
 14      min-height: 100vh;
 15      min-height: 100dvh;
 16      background: #0a0a0a;
 17      color: #e8e8f0;
 18      font-family: 'Outfit', sans-serif;
 19      display: flex;
 20      flex-direction: column;
 21      align-items: center;
 22      justify-content: center;
 23      overflow-x: hidden;
 24      overflow-y: auto;
 25    }
 26  
 27    /* Fullscreen slideshow background */
 28    .slideshow {
 29      position: fixed;
 30      inset: 0;
 31      z-index: 0;
 32    }
 33  
 34    .slideshow img {
 35      position: absolute;
 36      inset: 0;
 37      width: 100%;
 38      height: 100%;
 39      object-fit: cover;
 40      opacity: 0;
 41      transition: opacity 2s ease-in-out;
 42    }
 43  
 44    .slideshow img.active {
 45      opacity: 1;
 46    }
 47  
 48    /* Dark overlay so text is readable */
 49    .slideshow::after {
 50      content: '';
 51      position: absolute;
 52      inset: 0;
 53      background: radial-gradient(ellipse at center, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.75) 100%);
 54    }
 55  
 56    .container {
 57      width: 100%;
 58      max-width: 560px;
 59      padding: 27px 32px;
 60      display: flex;
 61      flex-direction: column;
 62      align-items: center;
 63      gap: 27px;
 64      position: relative;
 65      z-index: 1;
 66    }
 67  
 68    /* Thumbnail strip showing all images */
 69    .thumb-strip {
 70      display: flex;
 71      gap: 10px;
 72      justify-content: center;
 73    }
 74  
 75    .thumb-strip img {
 76      width: 120px;
 77      height: 68px;
 78      object-fit: cover;
 79      border-radius: 8px;
 80      opacity: 0.4;
 81      transition: opacity 0.4s, box-shadow 0.4s;
 82      cursor: pointer;
 83      border: 2px solid transparent;
 84    }
 85  
 86    .thumb-strip img.active {
 87      opacity: 1;
 88      border-color: rgba(255, 165, 0, 0.7);
 89      box-shadow: 0 0 16px rgba(255, 165, 0, 0.4);
 90    }
 91  
 92    .title-block { text-align: center; }
 93  
 94    .title-block .subtitle {
 95      font-family: 'Bebas Neue', sans-serif;
 96      font-size: 35px;
 97      font-weight: 400;
 98      letter-spacing: 10px;
 99      text-transform: uppercase;
100      color: rgba(255, 200, 150, 0.7);
101      margin-bottom: 5px;
102    }
103  
104    .title-block .title {
105      font-family: 'Bebas Neue', sans-serif;
106      font-size: 98px;
107      font-weight: 400;
108      letter-spacing: 10px;
109      text-transform: uppercase;
110      background: linear-gradient(135deg, #fff 0%, #ffb347 50%, #ff6b6b 100%);
111      -webkit-background-clip: text;
112      -webkit-text-fill-color: transparent;
113      background-clip: text;
114    }
115  
116    .progress-container { width: 100%; }
117  
118    .progress-bar {
119      width: 100%;
120      height: 33px;
121      cursor: pointer;
122      position: relative;
123      display: flex;
124      align-items: center;
125    }
126  
127    .progress-track {
128      width: 100%;
129      height: 14px;
130      background: rgba(255,255,255,0.08);
131      border-radius: 7px;
132      position: relative;
133    }
134  
135    .progress-fill {
136      height: 100%;
137      border-radius: 7px;
138      background: linear-gradient(90deg, #ff6b6b, #ffb347, #ffd700);
139      width: 0%;
140      position: relative;
141      pointer-events: none;
142    }
143  
144    .progress-bar.dragging .progress-fill,
145    .progress-bar.dragging .buffer-fill { transition: none; }
146  
147    .progress-thumb {
148      position: absolute;
149      top: 50%;
150      right: -16px;
151      transform: translateY(-50%);
152      width: 33px;
153      height: 33px;
154      background: #fff;
155      border-radius: 50%;
156      opacity: 0;
157      transition: opacity 0.15s;
158      box-shadow: 0 0 8px rgba(255,165,0,0.6);
159      cursor: grab;
160      pointer-events: auto;
161      z-index: 2;
162    }
163  
164    .progress-bar:hover .progress-thumb,
165    .progress-bar.dragging .progress-thumb { opacity: 1; }
166  
167    .progress-bar.dragging .progress-thumb { cursor: grabbing; transform: translateY(-50%) scale(1.2); }
168  
169    .buffer-fill {
170      position: absolute;
171      top: 0;
172      left: 0;
173      height: 100%;
174      border-radius: 7px;
175      background: rgba(255,255,255,0.06);
176      width: 0%;
177    }
178  
179    .time-row {
180      display: flex;
181      justify-content: space-between;
182      margin-top: 10px;
183      font-size: 28px;
184      color: rgba(255,200,150,0.5);
185      font-variant-numeric: tabular-nums;
186    }
187  
188    .controls {
189      display: flex;
190      align-items: center;
191      gap: 56px;
192    }
193  
194    .btn {
195      background: none;
196      border: none;
197      color: #e8e8f0;
198      cursor: pointer;
199      display: flex;
200      align-items: center;
201      justify-content: center;
202      transition: color 0.15s, transform 0.1s;
203    }
204  
205    .btn:hover { color: #ffb347; }
206    .btn:active { transform: scale(0.93); }
207  
208    .btn-play {
209      width: 210px;
210      height: 210px;
211      background: linear-gradient(135deg, #ff6b6b, #ff8e53);
212      border-radius: 50%;
213      box-shadow: 0 5px 27px rgba(255,107,107,0.4);
214      transition: transform 0.15s, box-shadow 0.15s;
215    }
216  
217    .btn-play svg {
218      width: 88px;
219      height: 88px;
220    }
221  
222    .btn-play:hover {
223      transform: scale(1.05);
224      box-shadow: 0 6px 28px rgba(255,107,107,0.6);
225      color: #fff;
226    }
227  
228    .volume-container {
229      display: flex;
230      flex-direction: column;
231      align-items: center;
232      gap: 6px;
233    }
234  
235    .volume-slider {
236      -webkit-appearance: slider-vertical;
237      appearance: slider-vertical;
238      writing-mode: vertical-lr;
239      direction: rtl;
240      width: 6px;
241      height: 140px;
242      background: rgba(255,255,255,0.15);
243      border-radius: 2px;
244      outline: none;
245      cursor: pointer;
246    }
247  
248    .volume-slider::-webkit-slider-thumb {
249      -webkit-appearance: none;
250      width: 24px;
251      height: 24px;
252      background: #ffb347;
253      border-radius: 50%;
254      cursor: pointer;
255    }
256  
257    .volume-slider::-moz-range-thumb {
258      width: 24px;
259      height: 24px;
260      background: #ffb347;
261      border-radius: 50%;
262      cursor: pointer;
263      border: none;
264    }
265  
266    .status-msg {
267      font-size: 16px;
268      color: rgba(255,200,150,0.4);
269      min-height: 21px;
270    }
271  
272    .badges-bottom {
273      position: fixed;
274      bottom: 16px;
275      left: 0;
276      right: 0;
277      display: flex;
278      justify-content: center;
279      gap: 12px;
280      flex-wrap: wrap;
281      padding: 0 16px;
282      z-index: 10;
283    }
284  
285    .badge {
286      display: flex;
287      align-items: center;
288      gap: 8px;
289      opacity: 0.85;
290      transition: opacity 0.2s;
291      background: rgba(0,0,0,0.5);
292      padding: 10px 24px;
293      border-radius: 28px;
294      backdrop-filter: blur(8px);
295      -webkit-backdrop-filter: blur(8px);
296    }
297  
298    .badge:hover { opacity: 1; }
299  
300    .badge a {
301      display: flex;
302      align-items: center;
303      gap: 8px;
304      color: rgba(255,220,180,0.9);
305      text-decoration: none;
306      font-family: 'Bebas Neue', sans-serif;
307      font-size: 24px;
308      font-weight: 400;
309      letter-spacing: 3px;
310    }
311  
312    .badge svg { flex-shrink: 0; width: 36px; height: 36px; }
313  
314    /* Mobile: show images as a banner instead of fullscreen background */
315    @media (orientation: portrait) {
316      body {
317        justify-content: flex-start;
318      }
319  
320      .slideshow {
321        position: relative;
322        width: 100%;
323        aspect-ratio: 16 / 9;
324        flex-shrink: 0;
325      }
326  
327      .slideshow img {
328        object-fit: contain;
329        background: #0a0a0a;
330      }
331  
332      .slideshow::after {
333        display: none;
334      }
335  
336      .container {
337        max-width: 100%;
338        padding: 16px 16px;
339        gap: 20px;
340      }
341  
342      .thumb-strip {
343        width: 100%;
344        gap: 6px;
345      }
346  
347      .thumb-strip img {
348        flex: 1;
349        width: 0;
350        height: auto;
351        aspect-ratio: 16 / 9;
352      }
353  
354      .title-block .subtitle {
355        font-size: 42px;
356        letter-spacing: 12px;
357      }
358  
359      .title-block .title {
360        font-size: 110px;
361        letter-spacing: 14px;
362      }
363  
364      .btn-play {
365        width: 240px;
366        height: 240px;
367      }
368  
369      .btn-play svg {
370        width: 100px;
371        height: 100px;
372      }
373  
374      .volume-slider {
375        height: 200px;
376      }
377  
378      .volume-slider::-webkit-slider-thumb {
379        width: 36px;
380        height: 36px;
381      }
382  
383      .volume-slider::-moz-range-thumb {
384        width: 36px;
385        height: 36px;
386      }
387  
388      .volume-container svg {
389        width: 36px;
390        height: 36px;
391      }
392  
393      .controls {
394        gap: 60px;
395      }
396    }
397  </style>
398  </head>
399  <body>
400  
401  <!-- Fullscreen crossfading slideshow -->
402  <div class="slideshow" id="slideshow">
403    <img src="afrobeats_megastar_android_--chaos_10_--ar_169_--raw_--sref_3_b5460a51-3822-4345-ad8a-4b2b8b05c55d_1.png" class="active" alt="" />
404    <img src="Afro-futuristic_amapiano_DJ_at_an_open-air_beach_caf_poolside_e1ec3f33-58d1-45ed-9d2d-c1eb98601923_0.png" alt="" />
405    <img src="amapiano_dj_--chaos_10_--ar_169_--raw_--sref_2397594818_32016_df8faf50-173e-4474-9fe0-7db83d64dac4_0.png" alt="" />
406    <img src="group_of_fashionable_African_friends_relaxing_on_loungers_bes_80ecbab7-2bd2-4100-ab32-14a456ca3939_0.png" alt="" />
407  </div>
408  
409  <div class="container">
410    <div class="thumb-strip" id="thumbStrip">
411      <img src="afrobeats_megastar_android_--chaos_10_--ar_169_--raw_--sref_3_b5460a51-3822-4345-ad8a-4b2b8b05c55d_1.png" class="active" alt="" />
412      <img src="Afro-futuristic_amapiano_DJ_at_an_open-air_beach_caf_poolside_e1ec3f33-58d1-45ed-9d2d-c1eb98601923_0.png" alt="" />
413      <img src="amapiano_dj_--chaos_10_--ar_169_--raw_--sref_2397594818_32016_df8faf50-173e-4474-9fe0-7db83d64dac4_0.png" alt="" />
414      <img src="group_of_fashionable_African_friends_relaxing_on_loungers_bes_80ecbab7-2bd2-4100-ab32-14a456ca3939_0.png" alt="" />
415    </div>
416  
417    <div class="title-block">
418      <div class="subtitle">chillmix.eth</div>
419      <div class="title">Amapiano</div>
420    </div>
421  
422    <div class="progress-container">
423      <div class="progress-bar" id="progressBar">
424        <div class="progress-track">
425          <div class="buffer-fill" id="bufferFill"></div>
426          <div class="progress-fill" id="progressFill"><div class="progress-thumb" id="progressThumb"></div></div>
427        </div>
428      </div>
429      <div class="time-row">
430        <span id="currentTime">0:00</span>
431        <span id="duration">60:00</span>
432      </div>
433    </div>
434  
435    <div class="controls">
436      <button class="btn btn-play" id="playBtn" title="Play">
437        <svg id="iconPlay" width="50" height="50" viewBox="0 0 24 24" fill="currentColor">
438          <polygon points="6,3 20,12 6,21"/>
439        </svg>
440        <svg id="iconPause" width="50" height="50" viewBox="0 0 24 24" fill="currentColor" style="display:none">
441          <rect x="5" y="3" width="4" height="18"/><rect x="15" y="3" width="4" height="18"/>
442        </svg>
443      </button>
444  
445      <div class="volume-container">
446        <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
447          <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
448          <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/>
449        </svg>
450        <input type="range" class="volume-slider" id="volumeSlider" min="0" max="1" step="0.01" value="0.8" />
451      </div>
452    </div>
453  
454    <div class="status-msg" id="statusMsg"></div>
455  </div>
456  
457  <div class="badges-bottom">
458    <div class="badge">
459      <a href="https://www.ethswarm.org" target="_blank" rel="noopener">
460        <svg width="28" height="28" viewBox="0 0 1319.31 1341.67" fill="#F79E1B" xmlns="http://www.w3.org/2000/svg"><polygon opacity="0.45" points="1125.21 136.97 1125.21 293.82 985.79 215.4 985.79 58.54"/><polygon opacity="0.6" points="985.79 215.4 985.79 372.25 1125.21 293.82 1125.21 136.97"/><polygon opacity="0.3" points="67.5 1092.22 346.35 1249.07 625.21 1092.22 346.35 935.36"/><polygon opacity="0.45" points="346.35 935.36 346.35 1249.07 67.5 1092.22 67.5 778.51"/><polygon opacity="0.45" points="625.21 778.51 625.21 1092.22 346.35 935.36 346.35 621.65"/><polygon opacity="0.6" points="346.35 935.36 346.35 1249.07 625.21 1092.22 625.21 778.51"/><polygon opacity="0.6" points="67.5 778.51 67.5 1092.22 346.35 935.36 346.35 621.65"/><polygon opacity="0.8" points="654.65 407.11 794.08 485.53 933.5 407.11 794.08 328.68"/><polygon opacity="0.8" points="67.5 778.51 346.35 935.36 625.21 778.51 346.35 621.65"/><polygon opacity="0.3" points="691.46 1092.22 970.31 1249.07 1249.16 1092.22 970.31 935.36"/><polygon opacity="0.45" points="970.31 935.36 970.31 1249.07 691.46 1092.22 691.46 778.51"/><polygon opacity="0.45" points="1249.16 778.51 1249.16 1092.22 970.31 935.36 970.31 621.65"/><polygon opacity="0.6" points="970.31 935.36 970.31 1249.07 1249.16 1092.22 1249.16 778.51"/><polygon opacity="0.6" points="691.46 778.51 691.46 1092.22 970.31 935.36 970.31 621.65"/><polygon opacity="0.8" points="691.46 778.51 970.31 935.36 1249.16 778.51 970.31 621.65"/><polygon opacity="0.6" points="654.65 250.25 654.65 407.11 794.08 328.68 794.08 171.82"/><polygon opacity="0.3" points="375.8 563.96 654.65 720.81 933.5 563.96 654.65 407.11"/><polygon opacity="0.45" points="654.65 407.11 654.65 720.81 375.8 563.96 375.8 250.25"/><polygon opacity="0.6" points="375.8 250.25 375.8 563.96 654.65 407.11 654.65 93.4"/><polygon opacity="0.6" points="794.08 485.53 794.08 328.68 654.65 407.11 654.65 563.96 654.65 720.81 794.08 642.39 933.5 563.96 933.5 407.11"/><polygon opacity="0.45" points="794.08 171.82 654.65 93.4 654.65 407.11 933.5 563.96 933.5 407.11 794.08 328.68"/><polygon opacity="0.8" points="654.65 93.4 794.08 171.82 654.65 250.25 654.65 407.11 375.8 250.25"/><polygon opacity="0.8" points="846.36 136.97 985.79 215.4 1125.21 136.97 985.79 58.54"/></svg>
461        Hosted on Swarm
462      </a>
463    </div>
464    <div class="badge">
465      <a href="https://app.ens.domains/chillmix.eth" target="_blank" rel="noopener">
466        <svg width="28" height="28" viewBox="0 0 80 92" fill="#5298FF" xmlns="http://www.w3.org/2000/svg"><path d="M38.9515 0.848681L13.7395 42.3331C13.5418 42.6584 13.0824 42.6947 12.8372 42.4035C10.6176 39.7681 2.3486 28.5562 12.5807 18.3372C21.9175 9.01238 33.8099 2.36398 38.2174 0.0661478C38.7175 -0.194552 39.2442 0.367137 38.9515 0.848681Z"/><path d="M37.5575 91.1735C38.0606 91.5257 38.6807 90.9251 38.3434 90.4122C32.712 81.8469 13.9923 53.3481 11.4064 49.0701C8.85586 44.8504 3.83929 37.8378 3.4208 31.8381C3.37904 31.2391 2.55081 31.1175 2.34247 31.6808C2.00646 32.5892 1.64873 33.6734 1.31533 34.912C-2.89355 50.5471 3.21904 67.138 16.4943 76.4302L37.5575 91.1736V91.1735Z"/><path d="M41.0206 90.4359L66.2325 48.9514C66.4303 48.6261 66.8897 48.5899 67.1349 48.8811C69.3545 51.5165 77.6235 62.7284 67.3914 72.9473C58.0546 82.2722 46.1622 88.9206 41.7547 91.2184C41.2547 91.4791 40.7279 90.9174 41.0206 90.4359Z"/><path d="M42.4425 0.105463C41.9394 -0.246724 41.3194 0.35389 41.6566 0.866798C47.288 9.43208 66.0077 37.9309 68.5936 42.2089C71.1442 46.4286 76.1607 53.4412 76.5792 59.4409C76.621 60.0398 77.4492 60.1614 77.6576 59.5982C77.9936 58.6898 78.3513 57.6056 78.6847 56.367C82.8936 40.7319 76.781 24.141 63.5057 14.8488L42.4425 0.105463Z"/></svg>
467        Named by ENS
468      </a>
469    </div>
470    <div class="badge">
471      <a href="https://app.safe.global/home?safe=eth:0x44E4A48f65DB6503819259bFe0b73501a5A3a532" target="_blank" rel="noopener">
472        <svg width="28" height="28" viewBox="0 0 24 24" fill="#12FF80" xmlns="http://www.w3.org/2000/svg"><path d="M21.757 11.998h-2.485c-.742 0-1.343.579-1.343 1.293v3.47c0 .714-.602 1.293-1.344 1.293H6.699c-.743 0-1.344.578-1.344 1.292v2.391c0 .714.601 1.293 1.344 1.293h10.458c.742 0 1.335-.579 1.335-1.293V19.82c0-.714.602-1.22 1.344-1.22h1.92c.743 0 1.344-.58 1.344-1.293v-4.03c0-.714-.601-1.278-1.343-1.278Z"/><path d="M5.355 7.249c0-.714.6-1.293 1.343-1.293h9.88c.743 0 1.344-.579 1.344-1.293v-2.39c0-.714-.601-1.293-1.344-1.293H6.125c-.742 0-1.343.579-1.343 1.293v1.842c0 .714-.602 1.292-1.344 1.292H1.526C.784 5.407.182 5.986.182 6.7v4.034c0 .714.604 1.264 1.346 1.264h2.485c.743 0 1.344-.579 1.344-1.293L5.355 7.25Z"/><path d="M10.472 9.485h2.387c.778 0 1.409.608 1.409 1.356v2.296c0 .748-.632 1.356-1.41 1.356h-2.386c-.778 0-1.409-.608-1.409-1.356v-2.296c0-.749.632-1.356 1.409-1.356Z"/></svg>
473        Governed by Safe
474      </a>
475    </div>
476    <div class="badge">
477      <a href="https://app.radicle.xyz/nodes/seed.radicle.garden/rad:zhtbBPVoU1fnid91fX9i22tumbPF" target="_blank" rel="noopener">
478        <svg width="28" height="28" viewBox="0 0 44 44" fill="#825AC7" xmlns="http://www.w3.org/2000/svg"><g shape-rendering="crispEdges"><rect x="8" y="0" width="4" height="4"/><rect x="32" y="0" width="4" height="4"/><rect x="12" y="4" width="4" height="4"/><rect x="28" y="4" width="4" height="4"/><rect x="12" y="8" width="4" height="4"/><rect x="16" y="8" width="4" height="4"/><rect x="20" y="8" width="4" height="4"/><rect x="24" y="8" width="4" height="4"/><rect x="28" y="8" width="4" height="4"/><rect x="8" y="12" width="4" height="4"/><rect x="12" y="12" width="4" height="4"/><rect x="16" y="12" width="4" height="4"/><rect x="20" y="12" width="4" height="4"/><rect x="24" y="12" width="4" height="4"/><rect x="28" y="12" width="4" height="4"/><rect x="32" y="12" width="4" height="4"/><rect x="4" y="16" width="4" height="4"/><rect x="8" y="16" width="4" height="4"/><rect x="20" y="16" width="4" height="4"/><rect x="24" y="16" width="4" height="4"/><rect x="36" y="16" width="4" height="4"/><rect x="4" y="20" width="4" height="4"/><rect x="8" y="20" width="4" height="4"/><rect x="20" y="20" width="4" height="4"/><rect x="24" y="20" width="4" height="4"/><rect x="36" y="20" width="4" height="4"/><rect x="0" y="24" width="4" height="4"/><rect x="4" y="24" width="4" height="4"/><rect x="8" y="24" width="4" height="4"/><rect x="12" y="24" width="4" height="4"/><rect x="16" y="24" width="4" height="4"/><rect x="20" y="24" width="4" height="4"/><rect x="24" y="24" width="4" height="4"/><rect x="28" y="24" width="4" height="4"/><rect x="32" y="24" width="4" height="4"/><rect x="36" y="24" width="4" height="4"/><rect x="40" y="24" width="4" height="4"/><rect x="8" y="28" width="4" height="4"/><rect x="16" y="28" width="4" height="4"/><rect x="24" y="28" width="4" height="4"/><rect x="32" y="28" width="4" height="4"/><rect x="8" y="32" width="4" height="4"/><rect x="16" y="32" width="4" height="4"/><rect x="24" y="32" width="4" height="4"/><rect x="32" y="32" width="4" height="4"/><rect x="16" y="36" width="4" height="4"/><rect x="24" y="36" width="4" height="4"/><rect x="12" y="40" width="4" height="4"/><rect x="16" y="40" width="4" height="4"/><rect x="24" y="40" width="4" height="4"/><rect x="28" y="40" width="4" height="4"/></g></svg>
479        Source on Radicle
480      </a>
481    </div>
482  </div>
483  
484  <audio id="audio" preload="auto" src="AMAPIANO-Mix01d - 60min.mp3"></audio>
485  
486  <script>
487  const $ = id => document.getElementById(id);
488  const audio = $('audio');
489  const playBtn = $('playBtn');
490  const iconPlay = $('iconPlay');
491  const iconPause = $('iconPause');
492  const progressBar = $('progressBar');
493  const progressFill = $('progressFill');
494  const bufferFill = $('bufferFill');
495  const currentTimeEl = $('currentTime');
496  const durationEl = $('duration');
497  const volumeSlider = $('volumeSlider');
498  const statusMsg = $('statusMsg');
499  
500  const STORAGE_KEY = 'anton-amapiano-position';
501  const KNOWN_DURATION = 3600;
502  function getDuration() { return audio.duration && isFinite(audio.duration) ? audio.duration : KNOWN_DURATION; }
503  
504  function fmt(s) {
505    if (!s || !isFinite(s)) return '0:00';
506    const h = Math.floor(s / 3600);
507    const m = Math.floor((s % 3600) / 60);
508    const sec = Math.floor(s % 60);
509    if (h > 0) return h + ':' + String(m).padStart(2,'0') + ':' + String(sec).padStart(2,'0');
510    return m + ':' + String(sec).padStart(2,'0');
511  }
512  
513  // --- Slideshow ---
514  const slides = document.querySelectorAll('#slideshow img');
515  const thumbs = document.querySelectorAll('#thumbStrip img');
516  let currentSlide = 0;
517  let slideshowTimer;
518  
519  function showSlide(index) {
520    slides[currentSlide].classList.remove('active');
521    thumbs[currentSlide].classList.remove('active');
522    currentSlide = index % slides.length;
523    slides[currentSlide].classList.add('active');
524    thumbs[currentSlide].classList.add('active');
525  }
526  
527  function nextSlide() {
528    showSlide(currentSlide + 1);
529  }
530  
531  function resetSlideshowTimer() {
532    clearInterval(slideshowTimer);
533    slideshowTimer = setInterval(nextSlide, 60000);
534  }
535  
536  // Click thumbnails to jump to that slide
537  thumbs.forEach((thumb, i) => {
538    thumb.addEventListener('click', () => {
539      showSlide(i);
540      resetSlideshowTimer();
541    });
542  });
543  
544  resetSlideshowTimer();
545  
546  // --- Position save/restore ---
547  function savePosition() {
548    if (audio.currentTime > 0) {
549      localStorage.setItem(STORAGE_KEY, JSON.stringify({
550        time: audio.currentTime,
551        updated: Date.now()
552      }));
553    }
554  }
555  
556  function getSavedPosition() {
557    try {
558      const d = JSON.parse(localStorage.getItem(STORAGE_KEY));
559      return d && d.time > 5 ? d.time : 0;
560    } catch(e) { return 0; }
561  }
562  
563  function setStatus(msg) {
564    statusMsg.textContent = msg;
565    console.log('[audio]', msg);
566  }
567  
568  ['loadstart','loadeddata','loadedmetadata','canplay','canplaythrough','playing','waiting','stalled','suspend','error','abort','emptied','ended'].forEach(evt => {
569    audio.addEventListener(evt, () => {
570      const info = evt + ' | readyState=' + audio.readyState + ' | networkState=' + audio.networkState;
571      console.log('[audio event]', info);
572      if (evt === 'error' && audio.error) {
573        setStatus('Error: ' + audio.error.message + ' (code ' + audio.error.code + ')');
574      }
575      if (evt === 'waiting' || evt === 'stalled') setStatus('Buffering...');
576      if (evt === 'playing') setStatus('');
577    });
578  });
579  
580  const saved = getSavedPosition();
581  if (saved > 0) {
582    function restorePosition() {
583      audio.currentTime = saved;
584      currentTimeEl.textContent = fmt(saved);
585      progressFill.style.width = (saved / getDuration()) * 100 + '%';
586    }
587    if (audio.readyState >= 2) {
588      restorePosition();
589    } else {
590      currentTimeEl.textContent = fmt(saved);
591      progressFill.style.width = (saved / getDuration()) * 100 + '%';
592      audio.addEventListener('loadedmetadata', restorePosition, { once: true });
593    }
594  }
595  
596  // --- Play / Pause ---
597  function updatePlayIcon() {
598    iconPlay.style.display = audio.paused ? 'block' : 'none';
599    iconPause.style.display = audio.paused ? 'none' : 'block';
600  }
601  
602  function doPlay() {
603    const p = audio.play();
604    if (p && p.catch) p.catch(err => setStatus('Play failed: ' + err.message));
605  }
606  
607  playBtn.addEventListener('click', () => {
608    if (audio.paused) {
609      setStatus('Starting playback...');
610      doPlay();
611    } else {
612      audio.pause();
613    }
614  });
615  
616  audio.addEventListener('play', updatePlayIcon);
617  audio.addEventListener('pause', updatePlayIcon);
618  
619  // --- Duration ---
620  audio.addEventListener('loadedmetadata', () => {
621    durationEl.textContent = fmt(getDuration());
622    setStatus('Ready');
623  });
624  
625  // --- Progress + buffer ---
626  audio.addEventListener('timeupdate', () => {
627    const dur = getDuration();
628    progressFill.style.width = (audio.currentTime / dur) * 100 + '%';
629    currentTimeEl.textContent = fmt(audio.currentTime);
630  });
631  
632  audio.addEventListener('progress', () => {
633    if (audio.buffered.length > 0) {
634      const end = audio.buffered.end(audio.buffered.length - 1);
635      bufferFill.style.width = (end / getDuration()) * 100 + '%';
636    }
637  });
638  
639  // --- Seek ---
640  let dragging = false;
641  
642  function seekTo(clientX) {
643    const rect = progressBar.getBoundingClientRect();
644    const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
645    const time = pct * getDuration();
646    audio.currentTime = time;
647    progressFill.style.width = pct * 100 + '%';
648    currentTimeEl.textContent = fmt(time);
649  }
650  
651  progressBar.addEventListener('mousedown', (e) => {
652    dragging = true;
653    progressBar.classList.add('dragging');
654    seekTo(e.clientX);
655    e.preventDefault();
656  });
657  
658  document.addEventListener('mousemove', (e) => { if (dragging) seekTo(e.clientX); });
659  document.addEventListener('mouseup', () => { if (dragging) { dragging = false; progressBar.classList.remove('dragging'); } });
660  
661  progressBar.addEventListener('touchstart', (e) => {
662    dragging = true;
663    progressBar.classList.add('dragging');
664    seekTo(e.touches[0].clientX);
665  }, { passive: true });
666  
667  document.addEventListener('touchmove', (e) => { if (dragging) seekTo(e.touches[0].clientX); }, { passive: true });
668  document.addEventListener('touchend', () => { if (dragging) { dragging = false; progressBar.classList.remove('dragging'); } });
669  
670  // --- Keyboard skip ---
671  document.addEventListener('keydown', (e) => {
672    if (e.key === 'ArrowLeft') { audio.currentTime = Math.max(0, audio.currentTime - 15); e.preventDefault(); }
673    if (e.key === 'ArrowRight') { audio.currentTime = Math.min(getDuration(), audio.currentTime + 30); e.preventDefault(); }
674  });
675  
676  // --- Volume ---
677  audio.volume = 0.8;
678  volumeSlider.addEventListener('input', () => { audio.volume = parseFloat(volumeSlider.value); });
679  
680  // --- Auto-save ---
681  setInterval(savePosition, 2000);
682  window.addEventListener('beforeunload', savePosition);
683  document.addEventListener('visibilitychange', savePosition);
684  audio.addEventListener('pause', savePosition);
685  
686  // --- Media Session API ---
687  if ('mediaSession' in navigator) {
688    navigator.mediaSession.metadata = new MediaMetadata({
689      title: 'Amapiano Chill Mix',
690      artist: 'Anton',
691      album: '60 Minute Mix',
692      artwork: [{ src: 'afrobeats_megastar_android_--chaos_10_--ar_169_--raw_--sref_3_b5460a51-3822-4345-ad8a-4b2b8b05c55d_1.png', sizes: '1456x816', type: 'image/png' }]
693    });
694  
695    navigator.mediaSession.setActionHandler('play', () => doPlay());
696    navigator.mediaSession.setActionHandler('pause', () => audio.pause());
697    navigator.mediaSession.setActionHandler('seekbackward', () => { audio.currentTime = Math.max(0, audio.currentTime - 15); });
698    navigator.mediaSession.setActionHandler('seekforward', () => { audio.currentTime = Math.min(getDuration(), audio.currentTime + 30); });
699    navigator.mediaSession.setActionHandler('seekto', (d) => { audio.currentTime = d.seekTime; });
700  
701    audio.addEventListener('timeupdate', () => {
702      navigator.mediaSession.setPositionState({
703        duration: getDuration(),
704        playbackRate: audio.playbackRate,
705        position: audio.currentTime,
706      });
707    });
708  }
709  </script>
710  </body>
711  </html>