<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Bedtime Tunes — tunes to snooze to</title>
  <meta name="description" content="A curated bedtime playlist running since 2006. Quiet music for falling asleep, beneath the aurora." />
  <link rel="icon" type="image/png" href="/BT.png" />
  <meta property="og:type" content="website" />
  <meta property="og:site_name" content="Bedtime Tunes" />
  <meta property="og:title" content="Bedtime Tunes — tunes to snooze to" />
  <meta property="og:description" content="A curated bedtime playlist running since 2006." />
  <meta property="og:url" content="https://bedtimetunes.com/" />
  <meta property="og:image" content="https://audio.bedtimetunes.com/og.png" />
  <meta property="og:image:type" content="image/png" />
  <meta property="og:image:width" content="1200" />
  <meta property="og:image:height" content="630" />
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:title" content="Bedtime Tunes — tunes to snooze to" />
  <meta name="twitter:image" content="https://audio.bedtimetunes.com/og.png" />
  <meta name="theme-color" content="#1a0a1e" />
  <link href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;500;600&family=Josefin+Sans:wght@100;200;300&display=swap" rel="stylesheet" />
  <style>
    *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
    :root{--deep:#1a0a1e;--rose:#c2416b;--wine:#7b2650}
    html,body{height:100%;font-family:'Josefin Sans',sans-serif;background:var(--deep);color:#fff;overflow:hidden}
    #aurora{position:fixed;inset:0;z-index:0}
    #particles{position:fixed;inset:0;z-index:2;pointer-events:none}
    .stars{position:fixed;inset:0;z-index:1;pointer-events:none}
    .star{position:absolute;border-radius:50%;background:#fff;animation:twinkle var(--d,3s) ease-in-out infinite;animation-delay:var(--delay,0s)}
    @keyframes twinkle{0%,100%{opacity:var(--min-op,.1);transform:scale(1)}50%{opacity:var(--max-op,.8);transform:scale(1.3)}}
    @keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-12px)}}

    #app{position:relative;z-index:10;height:100vh;display:flex;gap:.7rem;padding:.65rem}
    /* ── LEFT: stage (2/3) ── */
    #stage{flex:2;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.8rem;min-width:0;padding:.5rem;overflow-y:auto;scrollbar-width:none}
    #stage::-webkit-scrollbar{display:none}
    .brand{font-family:'Barlow',sans-serif;font-weight:500;letter-spacing:.22em;text-transform:uppercase;font-size:1rem;opacity:.9}
    .brand small{display:block;font-size:.5rem;letter-spacing:.3em;opacity:.4;margin-top:.3rem}

    .now{width:100%;max-width:600px;display:flex;flex-direction:column;align-items:center;gap:.5rem}
    .playbar{width:100%;display:flex;flex-direction:column;align-items:center;gap:.7rem}
    /* media area swaps between mp3 art / youtube / spotify */
    .media{width:100%;display:flex;justify-content:center}
    #art-wrap{width:min(280px,60vw);height:min(280px,60vw);border-radius:14px;overflow:hidden;background:url('/bedtimetunes.jpg') center/cover no-repeat,linear-gradient(135deg,#c2416b,#7b2650);box-shadow:0 0 80px rgba(192,65,107,.5),0 20px 60px rgba(0,0,0,.6);animation:float 8s ease-in-out infinite;transition:box-shadow .6s}
    #art-wrap.playing{box-shadow:0 0 110px rgba(192,65,107,.75),0 0 200px rgba(192,65,107,.2),0 20px 60px rgba(0,0,0,.6)}
    #art{width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .6s}#art.on{opacity:1}
    .now.wide{max-width:min(920px,94vw)}
    #yt-wrap{width:100%;max-width:min(820px,calc((100vh - 380px)*16/9));max-height:calc(100vh - 380px);aspect-ratio:16/9;border-radius:12px;overflow:hidden;background:#000 url('/bedtimetunes.jpg') center/cover no-repeat;box-shadow:0 20px 60px rgba(0,0,0,.6)}
    #yt-host{width:100%;height:100%}
    #sp-wrap{width:100%;max-width:480px}
    #sp-host{width:100%;min-height:152px;border-radius:12px;overflow:hidden}
    .hidden{display:none!important}

    .meta{text-align:center;width:100%}
    #t-title{font-family:'Barlow',sans-serif;font-weight:500;font-size:clamp(1.2rem,3vw,1.6rem);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
    #t-artist{font-size:.6rem;letter-spacing:.25em;text-transform:uppercase;color:rgba(255,255,255,.5);margin-top:.35rem}
    #t-genre{font-size:.5rem;letter-spacing:.2em;text-transform:uppercase;color:rgba(255,255,255,.28);margin-top:.3rem}
    .src-badge{display:inline-block;margin-top:.5rem;font-family:'Barlow',sans-serif;font-size:.5rem;letter-spacing:.2em;text-transform:uppercase;padding:.2rem .6rem;border-radius:100px;border:1px solid rgba(255,255,255,.15);color:rgba(255,255,255,.55)}
    #notice{font-size:.62rem;letter-spacing:.06em;color:rgba(232,160,90,.95);margin-top:.4rem;min-height:0;text-align:center}
    #notice:empty{margin-top:0}
    #notice a{color:#fff;text-decoration:underline}
    .estimate{font-size:.5rem;letter-spacing:.18em;text-transform:uppercase;color:rgba(255,255,255,.35);padding:.1rem .9rem .4rem}
    .ver{opacity:.4;font-size:.9em}
    .switch-btn{display:inline-block;margin:0;background:rgba(255,255,255,.05);border:1px solid rgba(194,65,107,.5);color:rgba(255,255,255,.8);border-radius:100px;font-family:'Barlow',sans-serif;font-size:.5rem;letter-spacing:.16em;text-transform:uppercase;padding:.3rem .8rem;cursor:pointer;transition:all .2s}
    .switch-btn:hover{background:rgba(194,65,107,.25);color:#fff;border-color:rgba(194,65,107,.8)}
    .pill-row{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:.7rem;margin-top:.45rem;width:100%}
    .story-row{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:.7rem;margin-top:.4rem}
    .pill-cta{background:linear-gradient(135deg,rgba(194,65,107,.4),rgba(123,38,80,.4));border:1px solid rgba(194,65,107,.6);color:#fff;border-radius:100px;font-family:'Barlow',sans-serif;font-size:.5rem;letter-spacing:.16em;text-transform:uppercase;padding:.3rem .85rem;cursor:pointer;transition:all .2s;animation:pillglow 3.5s ease-in-out infinite}
    .pill-cta:hover{background:linear-gradient(135deg,rgba(194,65,107,.65),rgba(123,38,80,.65));box-shadow:0 0 24px rgba(194,65,107,.6)}
    .pill-cta.alt{background:linear-gradient(135deg,rgba(224,144,44,.32),rgba(212,102,58,.32));border-color:rgba(224,144,44,.6);animation:none}
    .pill-cta.alt:hover{background:linear-gradient(135deg,rgba(224,144,44,.55),rgba(212,102,58,.55));box-shadow:0 0 24px rgba(224,144,44,.55)}
    @keyframes pillglow{0%,100%{box-shadow:0 0 0 rgba(194,65,107,0)}50%{box-shadow:0 0 16px rgba(194,65,107,.4)}}
    .uploader{display:flex;align-items:center;justify-content:center;gap:.45rem;margin-top:0;font-family:'Barlow',sans-serif;font-size:.5rem;letter-spacing:.16em;text-transform:uppercase;color:rgba(255,255,255,.4)}
    .uploader:empty{display:none}
    .desc-wrap{position:relative;display:inline-block;margin-top:0}
    .desc-btn{background:rgba(255,255,255,.05);border:1px solid rgba(194,65,107,.45);color:rgba(255,255,255,.75);border-radius:100px;font-family:'Barlow',sans-serif;font-size:.5rem;letter-spacing:.18em;text-transform:uppercase;padding:.3rem .85rem;cursor:pointer;transition:all .2s}
    .desc-btn:hover{background:rgba(194,65,107,.25);color:#fff;border-color:rgba(194,65,107,.8)}
    .desc-pop{position:absolute;left:50%;top:calc(100% + .55rem);transform:translateX(-50%) translateY(-4px);width:min(300px,80vw);background:rgba(26,10,30,.97);border:1px solid rgba(255,255,255,.14);border-radius:14px;padding:.85rem 1rem;font-size:.8rem;line-height:1.5;color:rgba(255,255,255,.85);font-style:italic;text-align:center;box-shadow:0 20px 50px rgba(0,0,0,.55);opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;z-index:60}
    .desc-pop::before{content:'';position:absolute;left:50%;top:-6px;transform:translateX(-50%) rotate(45deg);width:11px;height:11px;background:rgba(26,10,30,.97);border-left:1px solid rgba(255,255,255,.14);border-top:1px solid rgba(255,255,255,.14)}
    .desc-wrap.open .desc-pop,.desc-wrap:hover .desc-pop{opacity:1;transform:translateX(-50%) translateY(0);pointer-events:auto}
    .uploader a{color:rgba(255,255,255,.72);text-decoration:none;border-bottom:1px solid rgba(255,255,255,.22)}
    .uploader a:hover{color:#fff}
    .up-loc{color:rgba(255,255,255,.32)}
    .up-photo{width:20px;height:20px;border-radius:50%;object-fit:cover;border:1px solid rgba(255,255,255,.2)}
    #splash{position:fixed;inset:0;z-index:200;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1.2rem;background:var(--deep);transition:opacity .7s}
    #splash img{width:min(220px,55vw);height:min(220px,55vw);object-fit:cover;border-radius:22px;box-shadow:0 0 90px rgba(192,65,107,.55),0 20px 60px rgba(0,0,0,.6);animation:float 8s ease-in-out infinite}
    #splash.gone{opacity:0;pointer-events:none}
    .splash-sub{font-family:'Barlow',sans-serif;font-size:.55rem;letter-spacing:.34em;text-transform:uppercase;color:rgba(255,255,255,.5)}
    .modal{position:fixed;inset:0;z-index:300;display:flex;align-items:center;justify-content:center;padding:1.5rem;background:rgba(8,3,10,.72);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px)}
    .modal-card{position:relative;width:100%;max-width:400px;background:rgba(26,10,30,.92);border:1px solid rgba(255,255,255,.12);border-radius:18px;padding:1.8rem;display:flex;flex-direction:column;gap:.9rem;box-shadow:0 30px 80px rgba(0,0,0,.6)}
    .modal-card h2{font-family:'Barlow',sans-serif;font-weight:500;letter-spacing:.18em;text-transform:uppercase;font-size:.95rem}
    .modal-card p{font-size:.85rem;line-height:1.5;color:rgba(255,255,255,.7)}
    .modal-x{position:absolute;top:.7rem;right:.9rem;background:none;border:none;color:rgba(255,255,255,.5);font-size:1.4rem;cursor:pointer;line-height:1}
    .modal-x:hover{color:#fff}
    .modal-apply{display:inline-block;text-align:center;margin-top:.4rem;background:linear-gradient(135deg,#c2416b,#7b2650);color:#fff;border-radius:100px;padding:.7rem;font-family:'Barlow',sans-serif;letter-spacing:.18em;text-transform:uppercase;font-size:.6rem;text-decoration:none}
    .modal-apply:hover{box-shadow:0 0 30px rgba(194,65,107,.6)}

    .progress{width:100%;max-width:460px;display:flex;flex-direction:column;gap:.35rem;margin:0 auto}
    #bar{width:100%;-webkit-appearance:none;appearance:none;height:3px;border-radius:2px;background:rgba(255,255,255,.12);cursor:pointer;outline:none}
    #bar::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:#c2416b;box-shadow:0 0 8px rgba(194,65,107,.8)}
    .times{display:flex;justify-content:space-between;font-size:.55rem;color:rgba(255,255,255,.35)}

    .controls{display:flex;align-items:center;justify-content:center;gap:2rem}
    .cbtn{background:none;border:none;cursor:pointer;color:rgba(255,255,255,.55);transition:color .2s,transform .15s;display:flex}
    .cbtn:hover{color:#fff;transform:scale(1.15)}
    .cbtn.play{width:56px;height:56px;border-radius:50%;background:linear-gradient(135deg,#c2416b,#7b2650);color:#fff;justify-content:center;align-items:center;box-shadow:0 0 30px rgba(194,65,107,.6)}
    .cbtn.play:hover{box-shadow:0 0 50px rgba(194,65,107,.9)}
    .cbtn.active{color:#c2416b}
    .volume{display:flex;align-items:center;gap:.6rem;width:100%;max-width:280px}
    #vol{flex:1;-webkit-appearance:none;height:3px;border-radius:2px;background:rgba(255,255,255,.12);outline:none}
    #vol::-webkit-slider-thumb{-webkit-appearance:none;width:10px;height:10px;border-radius:50%;background:rgba(255,255,255,.6)}

    /* ── RIGHT: sidebar list (1/3) ── */
    #sidebar{flex:1;min-width:280px;max-width:420px;display:flex;flex-direction:column;background:rgba(26,10,30,.55);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,.08);border-radius:18px;overflow:hidden}
    .side-head{padding:1rem 1rem .6rem;display:flex;flex-direction:column;gap:.6rem}
    .side-title{font-family:'Barlow',sans-serif;font-size:.6rem;letter-spacing:.25em;text-transform:uppercase;color:rgba(255,255,255,.4);display:flex;justify-content:space-between;align-items:center}
    .toggles{display:flex;gap:.4rem}
    .tgl{display:inline-flex;align-items:center;gap:.4em;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.14);color:rgba(255,255,255,.4);border-radius:100px;font-family:'Barlow',sans-serif;font-size:.5rem;letter-spacing:.18em;text-transform:uppercase;padding:.28rem .65rem;cursor:pointer;transition:all .2s}
    .tgl::before{content:'';width:6px;height:6px;border-radius:50%;border:1px solid currentColor;background:transparent;flex-shrink:0;transition:all .2s}
    .tgl:hover{color:rgba(255,255,255,.72);border-color:rgba(255,255,255,.26)}
    .tgl.on{color:#fff;border-color:rgba(194,65,107,.7);background:rgba(194,65,107,.28);box-shadow:0 0 14px rgba(194,65,107,.35)}
    .tgl.on::before{background:#e8739a;border-color:#e8739a;box-shadow:0 0 7px rgba(232,115,154,.9)}
    #search{width:100%;background:rgba(0,0,0,.25);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:.5rem .7rem;color:#fff;font-family:'Josefin Sans';font-size:.85rem;outline:none}
    #search::placeholder{color:rgba(255,255,255,.3)}
    #list{flex:1;overflow-y:auto;padding:.3rem;scrollbar-width:thin;scrollbar-color:rgba(123,38,80,.6) transparent}
    /* themed scrollbars — semi-transparent dark, rounded, floating */
    #list::-webkit-scrollbar,body::-webkit-scrollbar{width:11px;height:11px}
    #list::-webkit-scrollbar-track,body::-webkit-scrollbar-track{background:transparent}
    #list::-webkit-scrollbar-thumb,body::-webkit-scrollbar-thumb{background:rgba(20,8,24,.55);border:3px solid transparent;background-clip:padding-box;border-radius:100px}
    #list::-webkit-scrollbar-thumb:hover,body::-webkit-scrollbar-thumb:hover{background:rgba(194,65,107,.55);background-clip:padding-box}
    body{scrollbar-width:thin;scrollbar-color:rgba(123,38,80,.6) transparent}
    .row{display:flex;align-items:center;gap:.6rem;padding:.45rem .6rem;border-radius:8px;cursor:pointer;transition:background .15s}
    .row:hover{background:rgba(255,255,255,.06)}
    .row.active{background:rgba(194,65,107,.16)}
    .row .src{flex-shrink:0;width:16px;text-align:center;font-size:.7rem;opacity:.7}
    .row .info{min-width:0;flex:1}
    .row .rt{font-size:.82rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
    .row .ra{font-size:.6rem;letter-spacing:.08em;color:rgba(255,255,255,.4);text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
    .row .rid{flex-shrink:0;font-size:.55rem;color:rgba(255,255,255,.2)}

    .foot{padding:.6rem 1rem;text-align:center;font-family:'Barlow',sans-serif;font-size:.5rem;letter-spacing:.25em;text-transform:uppercase;color:rgba(255,255,255,.25)}
    .foot a{color:rgba(255,255,255,.45);text-decoration:none}
    .logo-tip{position:relative;display:inline-block}
    .logo-tip .tip{position:absolute;bottom:-2.2rem;left:50%;transform:translateX(-50%);background:rgba(13,5,16,.85);border:1px solid rgba(255,255,255,.1);padding:.4rem .8rem;border-radius:100px;font-family:'Barlow';font-size:.5rem;letter-spacing:.18em;text-transform:uppercase;color:rgba(255,255,255,.7);white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .3s;z-index:50}
    .logo-tip:hover .tip{opacity:1}

    @media(max-width:820px){
      html,body{overflow-y:auto;overflow-x:hidden;height:auto}
      #app{display:flex;flex-direction:column;height:auto;min-height:100vh;gap:.6rem;padding:.6rem}
      /* flatten stage + now so their children become direct items of #app —
         this lets just the playbar be position:sticky across the whole page */
      #stage{display:contents}
      .now{display:contents}
      .logo-tip{align-self:center;margin-bottom:.2rem}
      .media{order:0}
      #art-wrap{width:min(150px,40vw);height:min(150px,40vw)}
      #yt-wrap{max-width:100%;max-height:34vh}
      .meta{order:0}
      /* only the play bar pins to the top; the playlist scrolls underneath it */
      .playbar{position:sticky;top:0;z-index:30;width:auto;margin:0 -.6rem;padding:.55rem .6rem;gap:.5rem;
               background:rgba(13,5,16,.9);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
               border-bottom:1px solid rgba(255,255,255,.08)}
      .volume{display:none}
      /* playlist gets the same bordered glass card as desktop */
      #sidebar{flex:none;min-width:0;max-width:none;margin-top:.4rem;
               background:rgba(26,10,30,.55);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
               border:1px solid rgba(255,255,255,.08);border-radius:18px;overflow:hidden}
      #list{overflow:visible}
    }
  </style>
</head>
<body>
<div id="splash"><img src="/bedtimetunes.jpg" alt="Bedtime Tunes"/><div class="splash-sub">loading tunes…</div></div>
<canvas id="aurora"></canvas><canvas id="particles"></canvas><div class="stars" id="stars"></div>

<div id="app">
  <!-- LEFT -->
  <div id="stage">
    <div class="logo-tip"><div class="brand">Bedtime Tunes<small>tunes to snooze to</small></div>
      <span class="tip">for you my friend &middot; Pedro &middot; Pit &#x271D; &lt;3</span></div>

    <div class="now">
      <div class="media">
        <div id="art-wrap"><img id="art" alt=""/></div>
        <div id="yt-wrap" class="hidden"><div id="yt-host"></div></div>
        <div id="sp-wrap" class="hidden"><div id="sp-host"></div></div>
      </div>
      <div class="meta">
        <div id="t-title">—</div>
        <div id="t-artist">—</div>
        <div id="t-genre"></div>
        <div class="story-row">
          <span id="desc-wrap" class="desc-wrap hidden">
            <button id="desc-btn" class="desc-btn" type="button">ℹ the story</button>
            <span id="desc-pop" class="desc-pop"></span>
          </span>
          <div id="uploader" class="uploader"></div>
        </div>
        <span class="src-badge" id="src-badge"></span>
        <div class="pill-row">
          <button id="switch" class="switch-btn hidden"></button>
          <button id="curator-pill" class="pill-cta" type="button">✦ become a curator</button>
          <button id="subscribe-pill" class="pill-cta alt" type="button">🔔 get new-song alerts</button>
        </div>
        <div id="notice"></div>
      </div>
      <div class="playbar">
        <div class="progress" id="mp3-progress">
          <input type="range" id="bar" min="0" max="100" value="0" step="0.1"/>
          <div class="times"><span id="t-cur">0:00</span><span id="t-tot">0:00</span></div>
        </div>
        <div class="controls">
          <button class="cbtn" id="prev" title="Previous">
            <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M18 6l-8.5 6L18 18V6zM8 6H6v12h2V6z"/></svg></button>
          <button class="cbtn play" id="play" title="Play/Pause">
            <svg id="ic-play" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
            <svg id="ic-pause" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" style="display:none"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg></button>
          <button class="cbtn" id="next" title="Next">
            <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zM16 6h2v12h-2V6z"/></svg></button>
          <button class="cbtn" id="share" title="Share this tune">
            <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M18 16a3 3 0 0 0-2.3 1.1l-7-4a3 3 0 0 0 0-2.2l7-4A3 3 0 1 0 15 5l-7 4a3 3 0 1 0 0 6l7 4A3 3 0 1 0 18 16z"/></svg></button>
        </div>
      </div>
      <div class="volume" id="mp3-volume">
        <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3z"/></svg>
        <input type="range" id="vol" min="0" max="100" value="80"/>
      </div>
    </div>
  </div>

  <!-- RIGHT -->
  <div id="sidebar">
    <div class="side-head">
      <div class="side-title"><span id="count">tunes</span><span class="toggles"><button id="reverse" class="tgl" title="Sort newest / oldest">Oldest first</button><button id="shuffle" class="tgl" title="Shuffle">Shuffle</button></span></div>
      <div id="estimate" class="estimate"></div>
      <input id="search" placeholder="Search title or artist…" />
    </div>
    <div id="list"></div>
    <div class="foot">made by <a href="https://lewi.fi" target="_blank" rel="noopener">lewi.fi</a> &middot;
      <a id="sp-playlist" href="#" target="_blank" rel="noopener">open in spotify ↗</a> &middot;
      <a href="https://audio.bedtimetunes.com/admin" target="_blank" rel="noopener">admin</a>
      <span id="ver" class="ver"></span></div>
  </div>
</div>

<div id="curator-modal" class="modal hidden">
  <div class="modal-card">
    <button id="curator-close" class="modal-x" aria-label="Close">&times;</button>
    <h2>Become a curator</h2>
    <p>Bedtime Tunes is curated by a small circle of friends. Want to add your own late-night tunes to the collection?</p>
    <p>Email us with one song you'd love to add — paste a YouTube link, or attach an MP3.</p>
    <a id="curator-apply" class="modal-apply" href="#">Apply by email ✉</a>
  </div>
</div>

<div id="subscribe-modal" class="modal hidden">
  <div class="modal-card">
    <button id="subscribe-close" class="modal-x" aria-label="Close">&times;</button>
    <h2>New-song alerts</h2>
    <p>Get a short email whenever a new tune is added to the collection. No spam, unsubscribe anytime.</p>
    <input id="subscribe-email" type="email" placeholder="you@example.com" style="width:100%;background:rgba(0,0,0,.25);border:1px solid rgba(255,255,255,.14);border-radius:10px;padding:.7rem .8rem;color:#fff;font-size:.95rem;outline:none"/>
    <button id="subscribe-go" class="modal-apply" type="button">Subscribe</button>
    <div id="subscribe-msg" style="font-size:.8rem;min-height:1rem;text-align:center;color:rgba(255,255,255,.7)"></div>
  </div>
</div>

<audio id="audio" preload="metadata"></audio>
<script src="https://www.youtube.com/iframe_api"></script>
<script src="https://open.spotify.com/embed/iframe-api/v1" async></script>
<script>
// ── CONFIG ──  ⚑ bump VERSION on every push (e.g. 3.0.3 → 3.0.4)
const VERSION='v3.0.21', BUILD_DATE='2026-06-18';
const AUDIO_BASE='https://audio.bedtimetunes.com', API_BASE='https://audio.bedtimetunes.com';
function track(type,track_id,meta){
  try{
    const body=JSON.stringify({type,track_id:track_id||null,meta:meta||null});
    const url=`${API_BASE}/api/event`;
    if(navigator.sendBeacon){navigator.sendBeacon(url,new Blob([body],{type:'text/plain'}));}
    else{fetch(url,{method:'POST',headers:{'Content-Type':'text/plain'},body,keepalive:true});}
  }catch(e){}
}
const SPOTIFY_PLAYLIST_URL='https://open.spotify.com/playlist/6AGKG8W0y4xezu8Cl6Rtkj';
document.getElementById('sp-playlist').href=SPOTIFY_PLAYLIST_URL;
document.getElementById('ver').textContent=VERSION;
// curator modal + prefilled application email
(function(){
  const m=document.getElementById('curator-modal');
  const subj="I want to apply to become a Bedtime Tunes curator";
  const body="Give one example of a song you'd like to add: (paste a YouTube link) — or attach an MP3 to this email.";
  document.getElementById('curator-apply').href=
    `mailto:contact@bedtimetunes.com?subject=${encodeURIComponent(subj)}&body=${encodeURIComponent(body)}`;
  document.getElementById('curator-pill').addEventListener('click',e=>{e.preventDefault();m.classList.remove('hidden');});
  document.getElementById('curator-close').addEventListener('click',()=>m.classList.add('hidden'));
  m.addEventListener('click',e=>{ if(e.target===m) m.classList.add('hidden'); });
  // subscribe modal
  const sm=document.getElementById('subscribe-modal');
  document.getElementById('subscribe-pill').addEventListener('click',e=>{e.preventDefault();sm.classList.remove('hidden');});
  document.getElementById('subscribe-close').addEventListener('click',()=>sm.classList.add('hidden'));
  sm.addEventListener('click',e=>{ if(e.target===sm) sm.classList.add('hidden'); });
  document.getElementById('subscribe-go').addEventListener('click',async()=>{
    const email=document.getElementById('subscribe-email').value.trim();
    const msg=document.getElementById('subscribe-msg');
    if(!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)){ msg.textContent='Please enter a valid email'; return; }
    msg.textContent='Subscribing…';
    try{
      const r=await fetch(`${API_BASE}/api/subscribe`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email})});
      msg.textContent = r.ok ? 'Subscribed ✓ — see you at bedtime.' : 'Something went wrong, try again.';
      if(r.ok) document.getElementById('subscribe-email').value='';
    }catch(e){ msg.textContent='Network error'; }
  });
})();
</script>
<script>
/* aurora + stars + particles (same shell) */
(function(){const c=document.getElementById('aurora'),x=c.getContext('2d');let W,H;function r(){W=c.width=innerWidth;H=c.height=innerHeight}r();
const orbs=[[194,65,107,.25,.30,.65,.50,.30],[139,26,107,.65,.55,.70,.55,.26],[110,60,180,.40,.18,.60,.45,.20],[60,100,200,.82,.40,.55,.42,.18],[235,120,70,.15,.65,.65,.50,.32],[255,110,40,.35,.45,.55,.42,.55],[245,170,80,.55,.78,.55,.42,.24],[215,60,70,.75,.85,.50,.40,.22],[255,140,100,.50,.50,.40,.32,.16]];
const cur=[[200,80,130,.32,.28,.45],[240,130,80,.52,.24,.38],[220,90,90,.70,.22,.32]];
function tint(){const h=new Date().getHours()+new Date().getMinutes()/60;const A=[[0,.75,.78,1.10],[7,1.15,.95,.85],[12,1,.95,.95],[17,1.15,.90,.80],[20,1.20,.85,.80],[24,.75,.78,1.10]];let a,b;for(let i=0;i<A.length-1;i++)if(h>=A[i][0]&&h<=A[i+1][0]){a=A[i];b=A[i+1];break}if(!a){a=A[0];b=A[1]}const t=(h-a[0])/(b[0]-a[0]);return[a[1]+(b[1]-a[1])*t,a[2]+(b[2]-a[2])*t,a[3]+(b[3]-a[3])*t]}
let T=tint();setInterval(()=>T=tint(),3e5);
function frame(ts){x.clearRect(0,0,W,H);const g=x.createRadialGradient(W*.5,H*.38,0,W*.5,H*.38,H);g.addColorStop(0,'#28102e');g.addColorStop(.6,'#1a0a22');g.addColorStop(1,'#0b0410');x.fillStyle=g;x.fillRect(0,0,W,H);x.globalCompositeOperation='lighter';
orbs.forEach((o,i)=>{const cx=W*(o[3]+Math.sin(ts*9e-5+i)*.18),cy=H*(o[4]+Math.sin(ts*7e-5+i)*.13),rx=W*o[5],ry=H*o[6];const r2=Math.min(255,o[0]*T[0]),g2=Math.min(255,o[1]*T[1]),b2=Math.min(255,o[2]*T[2]),al=o[7]+Math.sin(ts*1.2e-4+i)*.12;x.save();x.translate(cx,cy);x.scale(1,ry/rx);const gr=x.createRadialGradient(0,0,0,0,0,rx);gr.addColorStop(0,`rgba(${r2},${g2},${b2},${Math.max(0,al)})`);gr.addColorStop(.5,`rgba(${r2},${g2},${b2},${Math.max(0,al)*.3})`);gr.addColorStop(1,`rgba(${r2},${g2},${b2},0)`);x.fillStyle=gr;x.beginPath();x.arc(0,0,rx,0,7);x.fill();x.restore()});
cur.forEach((c2,i)=>{const yC=H*c2[3],hh=H*c2[4]*.5,ap=H*.09;x.beginPath();for(let px=0;px<=W+6;px+=6){const w=Math.sin(px*.0026+ts*15e-5+i)*ap;const y=yC+w-hh;px===0?x.moveTo(px,y):x.lineTo(px,y)}for(let px=W;px>=-6;px-=6){const w=Math.sin(px*.0026+ts*15e-5+i+.4)*ap;x.lineTo(px,yC+w+hh)}x.closePath();const gr=x.createLinearGradient(0,yC-hh*1.5,0,yC+hh*1.5);gr.addColorStop(0,`rgba(${c2[0]},${c2[1]},${c2[2]},0)`);gr.addColorStop(.5,`rgba(${c2[0]},${c2[1]},${c2[2]},${c2[5]})`);gr.addColorStop(1,`rgba(${c2[0]},${c2[1]},${c2[2]},0)`);x.fillStyle=gr;x.fill()});
x.globalCompositeOperation='source-over';rafId=requestAnimationFrame(frame)}let rafId=requestAnimationFrame(frame);
document.addEventListener('visibilitychange',()=>document.hidden?cancelAnimationFrame(rafId):rafId=requestAnimationFrame(frame));
let rt;addEventListener('resize',()=>{clearTimeout(rt);rt=setTimeout(r,100)})})();
(function(){const c=document.getElementById('stars'),n=innerWidth<600?70:160;for(let i=0;i<n;i++){const s=document.createElement('div');s.className='star';const z=Math.random()*2+.5;s.style.cssText=`width:${z}px;height:${z}px;top:${Math.random()*100}%;left:${Math.random()*100}%;--d:${(Math.random()*4+2).toFixed(1)}s;--delay:${(Math.random()*5).toFixed(1)}s;--min-op:${(Math.random()*.15+.05).toFixed(2)};--max-op:${(Math.random()*.5+.3).toFixed(2)}`;c.appendChild(s)}})();
(function(){const c=document.getElementById('particles'),x=c.getContext('2d');let W,H;const n=innerWidth<600?25:50,P=[];function r(){const d=Math.min(devicePixelRatio||1,2);W=c.width=innerWidth*d;H=c.height=innerHeight*d;c.style.width=innerWidth+'px';c.style.height=innerHeight+'px';x.setTransform(d,0,0,d,0,0)}let rt;addEventListener('resize',()=>{clearTimeout(rt);rt=setTimeout(r,150)});r();
function sp(p,fb){p.x=Math.random()*innerWidth;p.y=fb?innerHeight+Math.random()*50:Math.random()*innerHeight;p.s=Math.random()*1.5+.4;p.v=Math.random()*.15+.05;p.a=Math.random()*30+10;p.f=Math.random()*.001+5e-4;p.ph=Math.random()*7;p.al=Math.random()*.5+.15;p.bx=p.x}
for(let i=0;i<n;i++){const p={};sp(p,false);P.push(p)}let rafId;function frame(ts){x.clearRect(0,0,innerWidth,innerHeight);P.forEach(p=>{p.y-=p.v;p.x=p.bx+Math.sin(ts*p.f+p.ph)*p.a;x.beginPath();x.arc(p.x,p.y,p.s,0,7);x.fillStyle=`rgba(255,220,200,${(p.al*.7).toFixed(2)})`;x.fill();if(p.y<-10)sp(p,true)});rafId=requestAnimationFrame(frame)}rafId=requestAnimationFrame(frame);
document.addEventListener('visibilitychange',()=>document.hidden?cancelAnimationFrame(rafId):rafId=requestAnimationFrame(frame))})();
</script>
<script>
// ───────── PLAYER ─────────
const $=id=>document.getElementById(id);
const audio=$('audio');
let tracks=[], order=[], pos=-1, shuffleOn=false, reverseOn=false, source=null;
let ytPlayer=null, ytReady=false, spCtrl=null, spReady=false, spAdvanced=false;
const fmt=s=>{s=Math.floor(s||0);return `${Math.floor(s/60)}:${String(s%60).padStart(2,'0')}`};
const esc=s=>String(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
const sourceOf=t=> t.mp3_key?'mp3' : t.youtube_id?'youtube' : t.spotify_id?'spotify' : null;
const slug=s=>String(s||'').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'');
const pathFor=t=>`${t.id}-${slug(t.title)}-${slug(t.artist)}`;   // 147-honey-bee-zee-avi
const hashFor=t=>`#${pathFor(t)}`;
const srcIcon=s=> s==='mp3'?'♪' : s==='youtube'?'▶' : s==='spotify'?'⠿' : '·';

async function init(){
  try{ tracks=await (await fetch(`${API_BASE}/api/tracks`)).json(); }catch(e){ $('t-title').textContent='Could not load playlist'; $('splash').classList.add('gone'); return; }
  order=tracks.map((_,i)=>i);
  track('page');
  $('count').textContent=`${tracks.length} tunes`;
  // estimate total catalogue length from duration_ms
  const totalMs=tracks.reduce((s,t)=>s+(+t.duration_ms||0),0);
  const mins=Math.floor(totalMs/60000), d=Math.floor(mins/1440), h=Math.floor((mins%1440)/60), m=mins%60;
  $('estimate').textContent = totalMs ? `≈ ${d}d ${h}h ${m}m of music` : '';
  renderList();
  cueFromHash(false); // deep-link #id selects that track; otherwise nothing is selected
  setTimeout(()=>$('splash').classList.add('gone'), 300);
}
// map a track id (from #hash) to a position in the current order
function cueFromHash(autoplay){
  const h=location.hash;
  const id=(h.match(/^#?(\d+)/)||[])[1];   // id is the leading number
  if(!id) return false;
  const p=order.findIndex(oi=>String(tracks[oi].id)===id);
  if(p<0) return false;
  cue(p, autoplay);
  return true;
}
window.addEventListener('hashchange',()=>cueFromHash(true));

// YouTube / Spotify API ready hooks
window.onYouTubeIframeAPIReady=()=>{ ytPlayer=new YT.Player('yt-host',{height:'100%',width:'100%',
  playerVars:{autoplay:0,modestbranding:1,rel:0,playsinline:1},
  events:{onReady:()=>ytReady=true, onStateChange:e=>{
    if(source==='youtube') setIcon(e.data===YT.PlayerState.PLAYING);
    if(e.data===YT.PlayerState.ENDED) next();
  }}}); };
window.onSpotifyIframeApiReady=(API)=>{ API.createController($('sp-host'),{width:'100%',height:'152'},c=>{
  spCtrl=c; spReady=true;
  c.addListener('playback_update',e=>{
    if(source!=='spotify') return;
    setIcon(!e.data.isPaused);
    const p=e.data.position||0;
    if(p>=29000 && !spAdvanced){ spAdvanced=true; $('notice').innerHTML='Preview ended — <a href="'+SPOTIFY_PLAYLIST_URL+'" target="_blank">log in to Spotify</a> for the full track'; setTimeout(next,1200); }
  });
}); };

function setIcon(playing){
  $('ic-play').style.display = playing?'none':'';
  $('ic-pause').style.display = playing?'':'none';
  $('art-wrap').classList.toggle('playing', playing);
}
function showSource(s){
  source=s;
  $('art-wrap').classList.toggle('hidden', s!=='mp3');
  $('yt-wrap').classList.toggle('hidden', s!=='youtube');
  $('sp-wrap').classList.toggle('hidden', s!=='spotify');
  document.querySelector('.now').classList.toggle('wide', s==='youtube'); // give YT room
  const mp3=s==='mp3';
  $('mp3-progress').style.display=mp3?'':'none';
  $('mp3-volume').style.display=mp3?'':'none';
  $('play').style.display='';            // HTML play/pause now drives mp3, youtube & spotify
  $('src-badge').textContent=s?('source: '+s):'';
  setIcon(false);
}

function cue(p, autoplay, forceSrc){
  pos=((p%order.length)+order.length)%order.length;
  const t=tracks[order[pos]];
  const s=forceSrc || sourceOf(t);   // default = mp3-first; forceSrc = manual switch
  spAdvanced=false; $('notice').textContent='';
  $('t-title').textContent=t.title;
  $('t-artist').textContent=t.artist;
  document.title=`${t.title} — ${t.artist} · Bedtime Tunes`;
  $('t-genre').textContent=t.genre||'';
  const dw=$('desc-wrap'); dw.classList.remove('open');
  if(t.description){ $('desc-pop').textContent=t.description; dw.classList.remove('hidden'); }
  else dw.classList.add('hidden');
  const up=$('uploader');
  if(t.uploader_name){
    const ph=t.uploader_photo?`<img class="up-photo" src="${AUDIO_BASE}/${t.uploader_photo}" alt=""/>`:'';
    const nm=t.uploader_url?`<a href="${esc(t.uploader_url)}" target="_blank" rel="noopener">${esc(t.uploader_name)}</a>`:esc(t.uploader_name);
    const loc=t.uploader_location?` <span class="up-loc">· ${esc(t.uploader_location)}</span>`:'';
    up.innerHTML=`${ph}<span>added by ${nm}${loc}</span>`;
  } else up.innerHTML='';
  showSource(s);
  highlight();
  history.replaceState(null,'',location.pathname+hashFor(t)); // neat shareable URL with name
  if(autoplay) track('play', t.id, s);

  // offer a switch when the track has BOTH a self-hosted mp3 and a youtube video
  const sw=$('switch');
  if(t.mp3_key && t.youtube_id){
    sw.classList.remove('hidden');
    sw.textContent = s==='youtube' ? '♪ back to mp3' : '▶ switch to youtube';
  } else sw.classList.add('hidden');

  // resolved cover art (art_url = external e.g. spotify/youtube; art_key = our B2 image); logo shows behind if none
  const art=$('art'), img=t.art_url || (t.art_key?`${AUDIO_BASE}/${t.art_key}`:'');
  art.classList.remove('on');
  if(img){ art.src=img; art.onload=()=>art.classList.add('on'); } else { art.removeAttribute('src'); }

  // stop others
  audio.pause();
  if(ytPlayer&&ytReady&&s!=='youtube') try{ytPlayer.stopVideo()}catch(e){}
  if(spCtrl&&s!=='spotify') try{spCtrl.pause()}catch(e){}

  if(s==='mp3'){
    audio.src=`${AUDIO_BASE}/${t.mp3_key}`;
    if(autoplay) audio.play().catch(()=>{});
  } else if(s==='youtube'){
    const fn = autoplay ? 'loadVideoById' : 'cueVideoById';   // cue = load without autoplay
    if(ytReady) ytPlayer[fn](t.youtube_id);
    else setTimeout(()=>ytReady&&ytPlayer[fn](t.youtube_id),600);
  } else if(s==='spotify'){
    const load=()=>{ spCtrl.loadUri('spotify:track:'+t.spotify_id); if(autoplay) spCtrl.play(); };
    if(spReady) load(); else setTimeout(()=>spReady&&load(),600);
  }
}
function next(){ cue(pos+1, true); }
function prev(){ cue(pos-1, true); }

// transport — one play/pause button drives mp3, youtube and spotify
$('play').addEventListener('click',()=>{
  if(pos<0 || !source){ cue(0,true); return; }   // nothing selected yet → start at the top
  if(source==='mp3'){ audio.paused?audio.play():audio.pause(); }
  else if(source==='youtube' && ytReady){
    const st=ytPlayer.getPlayerState();
    (st===YT.PlayerState.PLAYING)?ytPlayer.pauseVideo():ytPlayer.playVideo();
  }
  else if(source==='spotify' && spCtrl){ spCtrl.togglePlay(); }
});
$('next').addEventListener('click',next);
$('prev').addEventListener('click',prev);
$('switch').addEventListener('click',()=>{ track('switch', tracks[order[pos]].id, source==='youtube'?'to-mp3':'to-youtube'); cue(pos, true, source==='youtube' ? 'mp3' : 'youtube'); });
$('desc-btn').addEventListener('click',e=>{ e.stopPropagation(); $('desc-wrap').classList.toggle('open'); });
document.addEventListener('click',()=>$('desc-wrap').classList.remove('open'));
$('share').addEventListener('click',async()=>{
  if(pos<0) return;
  const t=tracks[order[pos]];
  const url=`${API_BASE}/s/${pathFor(t)}`;   // OG-enabled share link → redirects to the player
  track('share', t.id);
  // native share sheet on mobile (Instagram/WhatsApp/Messages/"Add to Story" where supported)
  if(navigator.share){
    try{ await navigator.share({title:`${t.title} — ${t.artist}`, text:`${t.title} by ${t.artist} · Bedtime Tunes`, url}); return; }
    catch(e){ if(e&&e.name==='AbortError') return; }   // user cancelled — don't fall through to copy
  }
  try{ await navigator.clipboard.writeText(url); $('notice').textContent='link copied ✓'; setTimeout(()=>{if($('notice').textContent==='link copied ✓')$('notice').textContent='';},2000); }
  catch(e){ prompt('Copy this link:', url); }
});
audio.addEventListener('play',()=>setIcon(true));
audio.addEventListener('pause',()=>setIcon(false));
audio.addEventListener('ended',next);
audio.addEventListener('loadedmetadata',()=>{$('bar').max=audio.duration||100;$('t-tot').textContent=fmt(audio.duration)});
audio.addEventListener('timeupdate',()=>{$('bar').value=audio.currentTime;$('t-cur').textContent=fmt(audio.currentTime)});
$('bar').addEventListener('input',e=>audio.currentTime=+e.target.value);
$('vol').addEventListener('input',e=>audio.volume=e.target.value/100); audio.volume=.8;

// order toggles
$('shuffle').addEventListener('click',()=>{
  shuffleOn=!shuffleOn; $('shuffle').classList.toggle('on',shuffleOn);
  let o=tracks.map((_,i)=>i);
  if(shuffleOn){ for(let i=o.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[o[i],o[j]]=[o[j],o[i]];} }
  if(reverseOn) o.reverse();
  order=o;
  renderList($('search').value);
  cue(0,true);                        // play the song now at the top
  $('list').scrollTop=0;
});
$('reverse').addEventListener('click',()=>{
  reverseOn=!reverseOn;
  $('reverse').textContent = reverseOn ? 'Newest first' : 'Oldest first';
  $('reverse').classList.toggle('on',reverseOn);
  order=order.slice().reverse();      // flip whatever's showing (sorted or shuffled)
  renderList($('search').value);
  cue(0,true);                        // play the song now at the top
  $('list').scrollTop=0;              // keep the list scrolled to the top
});

// list + search
function renderList(filter){
  const list=$('list'); list.innerHTML='';
  const f=(filter||'').toLowerCase();
  order.forEach((ti,oi)=>{
    const t=tracks[ti];
    if(f && !(t.title.toLowerCase().includes(f)||t.artist.toLowerCase().includes(f))) return;
    const el=document.createElement('div'); el.className='row'; el.dataset.oi=oi;
    el.innerHTML=`<div class="src">${srcIcon(sourceOf(t))}</div>
      <div class="info"><div class="rt">${esc(t.title)}</div><div class="ra">${esc(t.artist)}</div></div>
      <div class="rid">${t.id}</div>`;
    el.addEventListener('click',()=>cue(oi,true));
    list.appendChild(el);
  });
  highlight();
}
function highlight(){
  document.querySelectorAll('.row').forEach(el=>{
    const on = pos>=0 && +el.dataset.oi===pos; el.classList.toggle('active',on);
    if(on) el.scrollIntoView({block:'nearest'});
  });
}
$('search').addEventListener('input',e=>renderList(e.target.value));

init();
</script>
</body>
</html>