> ## Knowledge Base Index
> Fetch the complete knowledge base index at: https://help.tako.id/sitemap.xml
> Use this file to discover available pages before exploring further.
> Pure-Markdown content can be obtained by appending a '.md' suffix to the content URLs listed in the sitemap (without the trailing slash).

# Cara Membuat Overlay Kustom

Mau *stream* kamu makin kece dengan overlay buatan sendiri? Custom overlay ini kayak kanvas kosong buat kamu berkreasi pake HTML. Bisa buat pesan spesial, animasi seru, atau branding yang bikin orang langsung inget sama kamu!

## Apa Saja Jenis Overlay yang Dapat Dikustomisasi?

1. **Alert** (Tidak termasuk media seperti Voice Note ataupun GIF)
2. **Media Share** (Hanya bagian pesan, bagian media/video dan progress bar tidak dapat di custom)
3. **Leaderboard**
4. **Timer** (Re-render tiap detik **kalau** kode kamu pake `{{timeLeft}}` atau `{{timeLeftMs}}`. Pake `{{timerEndAt}}` aja terus jalanin countdown sendiri di `<script>` — overlay render sekali, jalan sendiri, gak kedip)
5. **Soundboard**
6. **Milestone**
7. **Polling**
8. **Running Text**
9. **VIP Queue**
10. **Song Share** (Info lagu, antrian, dan player visual, progress bar, play/pause, cover art )

---

## Tips Penulisan Kode HTML

1. **Gunakan struktur HTML standar**: Selalu mulai dengan `<html>`, `<head>`, dan `<body>`.
2. **Jangan pake `position: absolute` di CSS Body**: Pastikan body memiliki height yang pasti karena sistem overlay Tako akan mendeteksi ukuran body, jika ukuran body tidak dapat di deteksi maka overlay akan tidak tampil karena sistem overlay Tako menganggap ukuran tinggi body sebesar 0px apabila body menggunakan posisi absolute.
3. **Gunain ukuran fleksibel**: Gunakan `px`, `rem`, atau `em` buat font atau elemen. **Jangan gunakan `vh` atau `vw`**.
4. **Biasakan gunakan ukuran `rem`** karena agar mudah di scaling melalui pengaturan ukuran font di dashboard pengaturan overlay.
5. Selalu tes di dashboard biar tampilan sesuai dengan harapan.
6. **Jangan pake komentar `//` di dalam `<script>`**: Sistem overlay Tako menghapus semua baris baru (newline) dari kode kamu sebelum dirender. Komentar `//` cuma berhenti di baris baru — begitu newline-nya hilang, komentar itu bisa "menelan" sisa kode di bawahnya sampai akhir script, bikin logic sesudahnya gak jalan tanpa ada error yang kelihatan. Kalo mau komentar, pake `/* ... */`, atau lebih aman gak usah pake komentar sama sekali.
7. **Data JSON (array/objek) langsung ditulis literal, jangan dibungkus tanda kutip**: Tulis `const data = {{namaVariabel}};` — BUKAN `const data = JSON.parse('{{namaVariabel}}')`. Isi JSON udah pasti pake tanda kutip dua (`"`), dan itu bakal motong string JS kamu di tengah kalo dibungkus kutip lagi, bikin script error.
8. **Jangan kasih `max-width` fixed di container utama**: Ukuran box overlay (lebar/tinggi) udah kamu tentuin sendiri lewat OBS Browser Source atau dashboard preview. Kalo kode kamu pake `max-width` tetap (misal `26rem`), box-nya bakal mentok di situ dan nyisain ruang kosong kalo box asli lebih lebar. Pake `width: 100%` biar ngisi penuh ukuran box yang udah kamu atur.
9. **Angka mentah ditulis literal, teks dibungkus kutip**: Variabel angka (misal `{{timerEndAt}}`, `{{timerAddedMs}}`, `{{hiddenCount}}`) tulis tanpa kutip — `const endAt = {{timerEndAt}};`. Variabel teks (misal `{{gifterName}}`, `{{timestamp}}`) baru dibungkus kutip — `const nama = "{{gifterName}}";`.

---

## Variabel untuk Setiap Overlay

Tiap overlay punya variabel yang bisa kamu pake biar tampilan dinamis tanpa edit manual. Berikut daftar lengkapnya, termasuk overlay yang mendukung dan contoh penggunaan:

⚠️ Pastikan tidak ada spasi antara kurung kurawal dengan nama variabel!

### 1. Alert & Media Share

| Nama Variabel | Contoh Info | Keterangan |
| ---- |
| `{{amount}}` | `15000` | Jumlah donasi (angka mentah). |
| `{{formattedAmount}}` | `IDR 15.000` | Jumlah donasi yang sudah diformat. |
| `{{message}}` | `Semangat streaming!` | Pesan dari donatur. |
| `{{gifterName}}` | `BudiGaming` | Nama donatur. |
| `{{gifterPicture}}` | https://example.com/profile.jpg | Link foto profil donatur. |
| `{{gifterBadges}}` | `VIP,TopDonator` | Daftar *badge* donatur (nama doang, dipisah koma) — **gak ada URL ikon** dari sistem. |
| `{{isGifterVerified}}` | `true` atau `false` | Status verifikasi donatur (`true` jika terverifikasi). |
| `{{mediaDuration}}` | `10` (dalam detik) | Durasi media pada alert/mediashare; jika tidak ada media, jadi durasi alertnya. |
| `{{timestamp}}` | `11 Sep 2026 12:29` | Waktu donasi, format `dd MMM yyyy HH:mm` di timezone kamu. Isinya sama persis kayak timestamp yang dicetak tema built-in di bawah kartu. |
| `{{showTimestamp}}` | `true` atau `false` | Status toggle "Munculkan Timestamp di Bawah Overlay?" di dashboard. Custom HTML dirender apa adanya, jadi kalo mau timestamp-nya ikut toggle ini, cek sendiri di `<script>` (lihat contoh kode). |

⚠️ Tako gak ngirim URL ikon badge. Kalo mau badge tampil sebagai **gambar** (bukan teks/tag), isi manual di `BADGE_ICON_URLS` dalam script di bawah: key = nama badge persis kayak yang keluar di `{{gifterBadges}}`, value = URL ikon badge itu. Badge yang gak ada di daftar itu otomatis fallback ke tag teks biasa.

Contoh kode di bawah udah diisiin 6 badge sistem resmi Tako (nama + URL):

| Nama Badge | URL Ikon |
| ---- |
| `Tako Verified Partner` | https://tako.id/images/badges/partnered-creator.png |
| `Tako Verified Creator` | https://tako.id/images/badges/verified-creator.png |
| `Rich Rich Club` | https://tako.id/images/badges/king.png |
| `Charity` | https://tako.id/images/badges/charity.png |
| `VTuber` | https://tako.id/images/badges/vtuber.png |
| `Tako Team` | https://tako.id/images/badges/admin.png |

**Contoh Kode:**

```html
<html>
<head>
  <style>
    body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
    .ticket {
      position: relative;
      width: 100%;
      box-sizing: border-box;
      overflow: hidden;
      background: #f2e9d3;
      border: 2px solid #1c1712;
      box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
      padding: 0.9rem 1rem 0.9rem 1.5rem;
    }
    .tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
    .grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
    .gifter-row { display: flex; align-items: center; gap: 0.6rem; }
    .avatar { width: 2.6rem; height: 2.6rem; border-radius: 50%; border: 2px solid #1c1712; object-fit: cover; flex-shrink: 0; display: none; }
    .avatar-fallback { width: 2.6rem; height: 2.6rem; border-radius: 50%; border: 2px solid #1c1712; background: #e8dab8; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; }
    .gifter-info { flex: 1; min-width: 0; }
    .gifter-name { font-weight: 700; font-size: 1.0625rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .badges { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; margin-top: 0.2rem; }
    .badge-tag { font-size: 0.75rem; border: 1px solid #7a7160; color: #7a7160; padding: 0.05rem 0.35rem; }
    .badge-icon { height: 1rem; width: auto; display: block; }
    .amount { font-weight: 700; font-size: 1.125rem; color: #b6842c; flex-shrink: 0; }
    .perf { border-bottom: 1px dashed #7a7160; margin: 0.6rem 0; }
    .message { font-size: 1rem; color: #4a4335; }
    .media-tag { display: inline-block; margin-top: 0.4rem; font-size: 0.75rem; color: #7a7160; border: 1px solid #7a7160; padding: 0.05rem 0.35rem; }
    .stamp { margin-top: 0.35rem; text-align: right; font-size: 0.75rem; letter-spacing: 0.04em; color: #7a7160; }
  </style>
</head>
<body>
  <div class="ticket">
    <div class="tab"></div>
    <div class="grommet"></div>

    <div class="gifter-row">
      <img class="avatar" id="avatar" alt="">
      <span class="avatar-fallback" id="avatar-fallback">🎁</span>
      <div class="gifter-info">
        <div class="gifter-name" id="gifter-name">{{gifterName}}</div>
        <div class="badges" id="badges"></div>
      </div>
      <div class="amount">{{formattedAmount}}</div>
    </div>
    <div class="perf"></div>
    <div class="message" id="message">{{message}}</div>
    <div class="media-tag" id="media-tag"></div>
    <div class="stamp" id="stamp">{{timestamp}}</div>
  </div>

  <script>
    const picture = "{{gifterPicture}}".trim();
    const badgesRaw = "{{gifterBadges}}".trim();
    const isVerified = "{{isGifterVerified}}" === "true";
    const mediaDuration = "{{mediaDuration}}";
    const showTimestamp = "{{showTimestamp}}" === "true";

    const BADGE_ICON_URLS = {
      "Tako Verified Partner": "https://tako.id/images/badges/partnered-creator.png",
      "Tako Verified Creator": "https://tako.id/images/badges/verified-creator.png",
      "Rich Rich Club": "https://tako.id/images/badges/king.png",
      "Charity": "https://tako.id/images/badges/charity.png",
      "VTuber": "https://tako.id/images/badges/vtuber.png",
      "Tako Team": "https://tako.id/images/badges/admin.png"
    };

    const avatar = document.getElementById("avatar");
    const avatarFallback = document.getElementById("avatar-fallback");
    const gifterName = document.getElementById("gifter-name");
    const badgesEl = document.getElementById("badges");
    const messageEl = document.getElementById("message");
    const mediaTag = document.getElementById("media-tag");
    const stampEl = document.getElementById("stamp");

    if (picture && !picture.includes("{{")) {
      avatar.src = picture;
      avatar.style.display = "block";
      avatarFallback.style.display = "none";
      avatar.onerror = () => {
        avatar.style.display = "none";
        avatarFallback.style.display = "flex";
      };
    }

    if (isVerified) gifterName.appendChild(document.createTextNode(" ✓"));

    if (badgesRaw && !badgesRaw.includes("{{")) {
      badgesRaw.split(",").filter(Boolean).forEach((name) => {
        const iconUrl = BADGE_ICON_URLS[name];

        if (iconUrl) {
          const icon = document.createElement("img");
          icon.className = "badge-icon";
          icon.src = iconUrl;
          icon.alt = name;
          icon.title = name;
          badgesEl.appendChild(icon);
        } else {
          const tag = document.createElement("span");
          tag.className = "badge-tag";
          tag.textContent = name;
          badgesEl.appendChild(tag);
        }
      });
    }

    if (!messageEl.textContent.trim() || messageEl.textContent.includes("{{")) {
      messageEl.style.display = "none";
    }

    if (mediaDuration && !mediaDuration.includes("{{") && Number(mediaDuration) > 0) {
      mediaTag.textContent = "⏱ " + mediaDuration + "s";
    } else {
      mediaTag.style.display = "none";
    }

    if (!showTimestamp || !stampEl.textContent.trim() || stampEl.textContent.includes("{{")) {
      stampEl.style.display = "none";
    }
  </script>
</body>
</html>
```

💡 `{{timestamp}}` dan `{{showTimestamp}}` jalan di Alert maupun Media Share. Nilai `{{showTimestamp}}` ngikutin toggle overlay yang sedang tampil — kalo hadiahnya mediashare, dia baca toggle di pengaturan Media Share, bukan Alert.

---

### 2. Leaderboard

| Nama Variabel | Contoh Info | Keterangan |
| ---- |
| `{{title}}` | `Top Donatur Bulan Ini` | Judul leaderboard dari pengaturan. |
| `{{rankings}}` | `[{"name":"BudiGaming","picture":"https://example.com/profile.jpg","badges":"VIP,TopDonator","amount":15000,"formattedAmount":"Rp15.000"}, ...]` | Daftar ranking (JSON): nama, foto, badge (nama doang, dipisah koma — **gak ada URL ikon**), jumlah donasi, jumlah donasi diformat. |

**Contoh Kode:**

```html
<html>
<head>
  <style>
    body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
    .ticket {
      position: relative;
      width: 100%;
      box-sizing: border-box;
      overflow: hidden;
      background: #f2e9d3;
      border: 2px solid #1c1712;
      box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
      padding: 0.9rem 1rem 0.9rem 1.5rem;
    }
    .tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
    .grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
    .title { font-size: 1.0625rem; font-weight: 700; }
    .perf { border-bottom: 1px dashed #7a7160; margin: 0.6rem 0; }
    .rank-row { display: flex; align-items: center; gap: 0.6rem; font-size: 1rem; padding: 0.35rem 0; border-bottom: 1px dashed #e8dab8; }
    .rank-row:last-child { border-bottom: none; }
    .rank-num { width: 1.6rem; font-weight: 700; color: #b6842c; flex-shrink: 0; }
    .rank-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .rank-badges { display: flex; align-items: center; gap: 0.2rem; flex-shrink: 0; }
    .rank-badge-tag { font-size: 0.75rem; border: 1px solid #7a7160; color: #7a7160; padding: 0.05rem 0.35rem; }
    .rank-badge-icon { height: 0.9375rem; width: auto; display: block; }
    .rank-amount { font-weight: 700; color: #7a7160; flex-shrink: 0; }
  </style>
</head>
<body>
  <div class="ticket">
    <div class="tab"></div>
    <div class="grommet"></div>

    <div class="title" id="lb-title">{{title}}</div>
    <div class="perf"></div>
    <div id="lb-list"></div>
  </div>

  <script>
    const rankings = {{rankings}};
    const list = document.getElementById("lb-list");

    const BADGE_ICON_URLS = {
      "Tako Verified Partner": "https://tako.id/images/badges/partnered-creator.png",
      "Tako Verified Creator": "https://tako.id/images/badges/verified-creator.png",
      "Rich Rich Club": "https://tako.id/images/badges/king.png",
      "Charity": "https://tako.id/images/badges/charity.png",
      "VTuber": "https://tako.id/images/badges/vtuber.png",
      "Tako Team": "https://tako.id/images/badges/admin.png"
    };

    rankings.forEach((r, i) => {
      const row = document.createElement("div");
      row.className = "rank-row";

      const num = document.createElement("span");
      num.className = "rank-num";
      num.textContent = "#" + (i + 1);

      const name = document.createElement("span");
      name.className = "rank-name";
      name.textContent = r.name;

      const badges = document.createElement("div");
      badges.className = "rank-badges";
      (r.badges || "").split(",").filter(Boolean).forEach((badgeName) => {
        const iconUrl = BADGE_ICON_URLS[badgeName];

        if (iconUrl) {
          const icon = document.createElement("img");
          icon.className = "rank-badge-icon";
          icon.src = iconUrl;
          icon.alt = badgeName;
          icon.title = badgeName;
          badges.appendChild(icon);
        } else {
          const tag = document.createElement("span");
          tag.className = "rank-badge-tag";
          tag.textContent = badgeName;
          badges.appendChild(tag);
        }
      });

      const amount = document.createElement("span");
      amount.className = "rank-amount";
      amount.textContent = r.formattedAmount;

      row.appendChild(num);
      row.appendChild(name);
      row.appendChild(badges);
      row.appendChild(amount);
      list.appendChild(row);
    });
  </script>
</body>
</html>
```

---

### 3. Milestone

| Nama Variabel | Contoh Info | Keterangan |
| ---- |
| `{{title}}` | `Keliling Nusantara` | Judul milestone dari pengaturan. |
| `{{currentAmount}}` | `75000` | Total donasi terkumpul dari semua langkah (angka mentah). |
| `{{formattedCurrentAmount}}` | `IDR 75.000` | Total donasi terkumpul, diformat. |
| `{{targetAmount}}` | `100000` | Total target dari semua langkah (angka mentah). |
| `{{formattedTargetAmount}}` | `IDR 100.000` | Total target, diformat. |
| `{{overallPercentage}}` | `75` | Persentase progres keseluruhan (0-100, bisa lebih dari 100 kalo kelebihan). |
| `{{stepProgress}}` | `25000` | Progres dalam langkah yang sedang aktif saja (angka mentah). |
| `{{formattedStepProgress}}` | `IDR 25.000` | Progres langkah aktif, diformat. |
| `{{stepTarget}}` | `50000` | Target langkah yang sedang aktif (angka mentah). |
| `{{formattedStepTarget}}` | `IDR 50.000` | Target langkah aktif, diformat. |
| `{{percentage}}` | `50` | Persentase progres langkah aktif (0-100). |
| `{{stepIndex}}` | `2` | Nomor urut langkah aktif (mulai dari 1). |
| `{{stepCount}}` | `4` | Total jumlah langkah. |
| `{{stepTitle}}` | `Langkah 2: Bali` | Judul langkah yang sedang aktif (kosong kalo gak diset). |
| `{{steps}}` / `{{milestoneSteps}}` | `[{"title":"Langkah 1","target":25000,"currentAmount":25000,"percentage":100,"isActive":false}, ...]` | Daftar semua langkah (JSON), tiap item punya `formattedTarget`/`formattedCurrentAmount`. |

**Contoh Kode:**

```html
<html>
<head>
  <style>
    body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
    .ticket {
      position: relative;
      width: 100%;
      box-sizing: border-box;
      overflow: hidden;
      background: #f2e9d3;
      border: 2px solid #1c1712;
      box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
      padding: 0.9rem 1rem 0.9rem 1.5rem;
    }
    .tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
    .grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
    .title { font-size: 1.0625rem; font-weight: 700; }
    .perf { border-bottom: 1px dashed #7a7160; margin: 0.6rem 0; }
    .bar-bg { height: 0.7rem; background: #e8dab8; border: 1px solid #1c1712; }
    .bar-fill { height: 100%; background: #b6842c; }
    .amounts { display: flex; justify-content: space-between; font-size: 1rem; font-weight: 700; margin-top: 0.45rem; }
    .meta { font-size: 0.8125rem; color: #7a7160; margin-top: 0.45rem; }
  </style>
</head>
<body>
  <div class="ticket">
    <div class="tab"></div>
    <div class="grommet"></div>

    <div class="title" id="ms-title">{{title}}</div>
    <div class="perf"></div>
    <div class="bar-bg">
      <div class="bar-fill" id="bar-fill" style="width: {{percentage}}%"></div>
    </div>
    <div class="amounts">
      <span id="current">{{formattedStepProgress}}</span>
      <span id="target">{{formattedStepTarget}}</span>
    </div>
    <div class="meta">Langkah {{stepIndex}} / {{stepCount}}</div>
  </div>
</body>
</html>
```

---

### 4. Polling

| Nama Variabel | Contoh Info | Keterangan |
| ---- |
| `{{title}}` | `Pilih Game Selanjutnya!` | Judul polling dari pengaturan. |
| `{{pollingOptions}}` | `[{"id":1,"name":"Game A","totalAmount":5000,"formattedTotalAmount":"Rp5.000"}, ...]` | Daftar opsi polling (JSON): ID, nama, jumlah donasi, jumlah donasi diformat. |
| `{{startAt}}` | `2025-07-08` | Tanggal mulai polling. |
| `{{startTime}}` | `09:30` | Waktu mulai polling. |
| `{{endAt}}` | `2025-07-08` | Tanggal selesai polling. |
| `{{endTime}}` | `12:30` | Waktu selesai polling. |

**Contoh Kode:**

```html
<html>
<head>
  <style>
    body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
    .ticket {
      position: relative;
      width: 100%;
      box-sizing: border-box;
      overflow: hidden;
      background: #f2e9d3;
      border: 2px solid #1c1712;
      box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
      padding: 0.9rem 1rem 0.9rem 1.5rem;
    }
    .tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
    .grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
    .title { font-size: 1.0625rem; font-weight: 700; }
    .perf { border-bottom: 1px dashed #7a7160; margin: 0.6rem 0; }
    .poll-row { display: flex; align-items: center; gap: 0.6rem; font-size: 1rem; padding: 0.35rem 0; border-bottom: 1px dashed #e8dab8; }
    .poll-row:last-child { border-bottom: none; }
    .poll-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .poll-amount { font-weight: 700; color: #7a7160; flex-shrink: 0; }
    .meta { font-size: 0.8125rem; color: #7a7160; margin-top: 0.55rem; }
  </style>
</head>
<body>
  <div class="ticket">
    <div class="tab"></div>
    <div class="grommet"></div>

    <div class="title" id="poll-title">{{title}}</div>
    <div class="perf"></div>
    <div id="poll-list"></div>
    <div class="meta">{{startAt}} {{startTime}} — {{endAt}} {{endTime}}</div>
  </div>

  <script>
    const options = {{pollingOptions}};
    const list = document.getElementById("poll-list");

    options.forEach((o) => {
      const row = document.createElement("div");
      row.className = "poll-row";

      const name = document.createElement("span");
      name.className = "poll-name";
      name.textContent = o.name;

      const amount = document.createElement("span");
      amount.className = "poll-amount";
      amount.textContent = o.formattedTotalAmount;

      row.appendChild(name);
      row.appendChild(amount);
      list.appendChild(row);
    });
  </script>
</body>
</html>
```

---

### 5. Running Text

| Nama Variabel | Contoh Info | Keterangan |
| ---- |
| `{{texts}}` | `[{"title":"Selamat datang!","backgroundColor":"#333","textColor":"#FFF"}, ...]` | Daftar teks tambahan (JSON): judul, warna latar, warna teks. |
| `{{gifts}}` | `[{"gifterName":"BudiGaming","amount":15000,"formattedAmount":"Rp15.000"}, ...]` | Daftar donasi (JSON): nama donatur, jumlah donasi, jumlah donasi diformat. |

**Contoh Kode:**

```html
<html>
<head>
  <style>
    body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; overflow: hidden; }
    .ticker {
      position: relative;
      width: 100%;
      box-sizing: border-box;
      background: #f2e9d3;
      border-top: 2px solid #1c1712;
      border-bottom: 2px solid #1c1712;
      padding: 0.6rem 0 0.6rem 1.4rem;
      overflow: hidden;
      white-space: nowrap;
    }
    .tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
    .grommet { position: absolute; left: 0.275rem; top: 50%; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, -50%); z-index: 2; }
    .ticker-track { display: inline-flex; gap: 1.5rem; animation: scroll 18s linear infinite; }
    .ticker-item { font-size: 1rem; font-weight: 700; flex-shrink: 0; }
    .ticker-item.gift { color: #9c3b2e; }
    @keyframes scroll {
      from { transform: translateX(0); }
      to { transform: translateX(-50%); }
    }
  </style>
</head>
<body>
  <div class="ticker">
    <div class="tab"></div>
    <div class="grommet"></div>
    <div class="ticker-track" id="track"></div>
  </div>

  <script>
    const texts = {{texts}};
    const gifts = {{gifts}};
    const track = document.getElementById("track");

    function buildItems() {
      const frag = document.createDocumentFragment();

      texts.forEach((t) => {
        const item = document.createElement("span");
        item.className = "ticker-item";
        item.style.color = t.textColor || "#1c1712";
        item.textContent = t.title;
        frag.appendChild(item);
      });

      gifts.forEach((g) => {
        const item = document.createElement("span");
        item.className = "ticker-item gift";
        item.textContent = g.gifterName + " — " + g.formattedAmount;
        frag.appendChild(item);
      });

      return frag;
    }

    track.appendChild(buildItems());
    track.appendChild(buildItems());
  </script>
</body>
</html>
```

Dua kali `buildItems()` di-append itu buat scroll seamless (dua salinan isi bersebelahan, `translateX(-50%)` berhenti pas salinan kedua persis nyampe posisi salinan pertama). Overlay ini gak ada progress-tick per-250ms kayak Song Share, jadi CSS animation loop aman jalan terus tanpa restart.

---

### 6. Soundboard

| Nama Variabel | Contoh Info | Keterangan |
| ---- |
| `{{amount}}` | `15000` | Jumlah donasi (angka mentah). |
| `{{formattedAmount}}` | `Rp15.000` | Jumlah donasi yang diformat. |
| `{{soundName}}` | `Explosion` | Nama efek suara yang dipilih. |
| `{{gifterName}}` | `BudiGaming` | Nama donatur. |
| `{{gifterPicture}}` | https://example.com/profile.jpg | Link foto profil donatur. |
| `{{gifterBadges}}` | `VIP,TopDonator` | Daftar *badge* donatur (nama doang, dipisah koma) — **gak ada URL ikon** dari sistem. |
| `{{isGifterVerified}}` | `true` atau `false` | Status verifikasi donatur (`true` jika terverifikasi). |

**Contoh Kode:**

```html
<html>
<head>
  <style>
    body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
    .ticket {
      position: relative;
      width: 100%;
      box-sizing: border-box;
      overflow: hidden;
      background: #f2e9d3;
      border: 2px solid #1c1712;
      box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
      padding: 0.9rem 1rem 0.9rem 1.5rem;
    }
    .tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
    .grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
    .gifter-row { display: flex; align-items: center; gap: 0.6rem; }
    .avatar { width: 2.6rem; height: 2.6rem; border-radius: 50%; border: 2px solid #1c1712; object-fit: cover; flex-shrink: 0; display: none; }
    .avatar-fallback { width: 2.6rem; height: 2.6rem; border-radius: 50%; border: 2px solid #1c1712; background: #e8dab8; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; }
    .gifter-info { flex: 1; min-width: 0; }
    .gifter-name { font-weight: 700; font-size: 1.0625rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .badges { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; margin-top: 0.2rem; }
    .badge-tag { font-size: 0.75rem; border: 1px solid #7a7160; color: #7a7160; padding: 0.05rem 0.35rem; }
    .badge-icon { height: 1rem; width: auto; display: block; }
    .amount { font-weight: 700; font-size: 1.125rem; color: #b6842c; flex-shrink: 0; }
    .perf { border-bottom: 1px dashed #7a7160; margin: 0.6rem 0; }
    .sound-name { font-size: 1.0625rem; font-weight: 700; }
  </style>
</head>
<body>
  <div class="ticket">
    <div class="tab"></div>
    <div class="grommet"></div>

    <div class="gifter-row">
      <img class="avatar" id="avatar" alt="">
      <span class="avatar-fallback" id="avatar-fallback">🔊</span>
      <div class="gifter-info">
        <div class="gifter-name" id="gifter-name">{{gifterName}}</div>
        <div class="badges" id="badges"></div>
      </div>
      <div class="amount">{{formattedAmount}}</div>
    </div>
    <div class="perf"></div>
    <div class="sound-name">🔈 {{soundName}}</div>
  </div>

  <script>
    const picture = "{{gifterPicture}}".trim();
    const badgesRaw = "{{gifterBadges}}".trim();
    const isVerified = "{{isGifterVerified}}" === "true";

    const BADGE_ICON_URLS = {
      "Tako Verified Partner": "https://tako.id/images/badges/partnered-creator.png",
      "Tako Verified Creator": "https://tako.id/images/badges/verified-creator.png",
      "Rich Rich Club": "https://tako.id/images/badges/king.png",
      "Charity": "https://tako.id/images/badges/charity.png",
      "VTuber": "https://tako.id/images/badges/vtuber.png",
      "Tako Team": "https://tako.id/images/badges/admin.png"
    };

    const avatar = document.getElementById("avatar");
    const avatarFallback = document.getElementById("avatar-fallback");
    const gifterName = document.getElementById("gifter-name");
    const badgesEl = document.getElementById("badges");

    if (picture && !picture.includes("{{")) {
      avatar.src = picture;
      avatar.style.display = "block";
      avatarFallback.style.display = "none";
      avatar.onerror = () => {
        avatar.style.display = "none";
        avatarFallback.style.display = "flex";
      };
    }

    if (isVerified) gifterName.appendChild(document.createTextNode(" ✓"));

    if (badgesRaw && !badgesRaw.includes("{{")) {
      badgesRaw.split(",").filter(Boolean).forEach((name) => {
        const iconUrl = BADGE_ICON_URLS[name];

        if (iconUrl) {
          const icon = document.createElement("img");
          icon.className = "badge-icon";
          icon.src = iconUrl;
          icon.alt = name;
          icon.title = name;
          badgesEl.appendChild(icon);
        } else {
          const tag = document.createElement("span");
          tag.className = "badge-tag";
          tag.textContent = name;
          badgesEl.appendChild(tag);
        }
      });
    }
  </script>
</body>
</html>
```

---

### 7. Timer

| Nama Variabel | Contoh Info | Keterangan |
| ---- |
| `{{title}}` | `Event Dimulai!` | Judul timer dari pengaturan. |
| `{{timeLeft}}` | `05:30` | Sisa waktu dalam format `MM:SS` (atau `00:00` jika habis). **Berubah tiap detik** — bikin overlay di-render ulang tiap detik. |
| `{{timeLeftMs}}` | `330000` | Sisa waktu dalam milidetik (angka mentah). **Berubah tiap detik** juga. |
| `{{timerEndAt}}` | `1757568540000` | Waktu timer habis dalam epoch milidetik (`0` = timer gak aktif / belum jalan). Nilainya **cuma berubah kalo waktu ditambah/dikurang/di-reset**, jadi kalo kode kamu pake ini aja (tanpa `{{timeLeft}}`/`{{timeLeftMs}}`), overlay render sekali dan countdown-nya kamu jalanin sendiri pake JS — animasi kamu gak kereset tiap detik. |
| `{{timerAddedMs}}` | `60000` | Perubahan waktu terakhir dalam milidetik. Minus = waktu dikurangi. `0` kalo belum ada perubahan sejak overlay dibuka. |
| `{{timerAddedSeconds}}` | `60` | Sama kayak di atas, dalam detik. |
| `{{timerAdded}}` | `+01:00` | Sama, tapi udah diformat lengkap dengan tanda plus/minus. Kosong kalo belum ada perubahan. |
| `{{isEnabled}}` | `true` atau `false` | Status timer (`true` jika aktif). |
| `{{options}}` | `[{"durationAdded":300,"minimumGiftAmount":10000}, ...]` | Daftar aturan pertambahan waktu (JSON): `durationAdded` = waktu yang ditambah dalam **detik**, `minimumGiftAmount` = jumlah donasi minimum buat dapat tambahan itu. Contoh di samping: donasi Rp10.000 nambah 300 detik (5 menit). |

💡 Alur pertambahan waktu: viewer donasi → Tako hitung dari aturan di "Aturan Pertambahan Waktu" → `{{timerEndAt}}` mundur sesuai tambahannya → `{{timerAddedSeconds}}` & `{{timerAdded}}` keisi jumlah yang baru ditambah. Jadi kalo aturannya Rp10.000 = 1 menit dan ada yang donasi Rp10.000, `{{timerAdded}}` bakal jadi `+01:00` — pas buat dipakein animasi "waktu nambah!".

**Contoh Kode:**

```html
<html>
<head>
  <style>
    body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
    .ticket {
      position: relative;
      width: 100%;
      box-sizing: border-box;
      overflow: hidden;
      background: #f2e9d3;
      border: 2px solid #1c1712;
      box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
      padding: 0.9rem 1rem 0.9rem 1.5rem;
      text-align: center;
    }
    .tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
    .grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
    .row-head { display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem; text-align: left; }
    .title { flex: 1; min-width: 0; font-size: 1.0625rem; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .status { flex-shrink: 0; font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: #7a7160; border: 1px solid #7a7160; padding: 0.1rem 0.4rem; }
    .perf { border-bottom: 1px dashed #7a7160; margin: 0.6rem 0; }
    .countdown { font-size: 2.25rem; font-weight: 700; letter-spacing: 0.05em; }
    .added { height: 1.35rem; font-size: 1.0625rem; font-weight: 700; color: #2f7d4f; opacity: 0; transition: opacity 0.25s ease-out; }
    .added.minus { color: #9c3b2e; }
    .added.show { opacity: 1; }
  </style>
</head>
<body>
  <div class="ticket">
    <div class="tab"></div>
    <div class="grommet"></div>

    <div class="row-head">
      <span class="title" id="timer-title">{{title}}</span>
      <span class="status" id="status-el">Active</span>
    </div>
    <div class="perf"></div>
    <div class="countdown" id="countdown">--:--</div>
    <div class="added" id="added">{{timerAdded}}</div>
  </div>

  <script>
    const endAt = {{timerEndAt}};
    const addedMs = {{timerAddedMs}};
    const isEnabled = "{{isEnabled}}" === "true";

    const countdown = document.getElementById("countdown");
    const added = document.getElementById("added");

    document.getElementById("status-el").textContent = isEnabled ? "Active" : "Inactive";

    const pad = (n) => String(n).padStart(2, "0");

    const tick = () => {
      const totalSeconds = endAt === 0 ? 0 : Math.max(0, Math.round((endAt - Date.now()) / 1000));
      const hours = Math.floor(totalSeconds / 3600);
      const minutes = Math.floor((totalSeconds % 3600) / 60);
      const seconds = totalSeconds % 60;

      countdown.textContent = hours > 0 ? pad(hours) + ":" + pad(minutes) + ":" + pad(seconds) : pad(minutes) + ":" + pad(seconds);
    };

    tick();
    setInterval(tick, 250);

    if (addedMs !== 0) {
      if (addedMs < 0) added.classList.add("minus");
      added.classList.add("show");
      setTimeout(() => added.classList.remove("show"), 1800);
    }
  </script>
</body>
</html>
```

Contoh di atas gak nyentuh `{{timeLeft}}` sama sekali, jadi overlay-nya render sekali doang — `setInterval` di dalam iframe yang ngurus countdown-nya. Animasi/transisi kamu jadi aman gak kereset tiap detik. Kalo kamu lebih suka simpel dan gak masalah overlay re-render tiap detik, tinggal ganti isi `.countdown` jadi `{{timeLeft}}` dan buang script countdown-nya.

---

### 8. VIP Queue

| Nama Variabel | Contoh Info | Keterangan |
| ---- |
| `{{title}}` | `Antrian VIP` | Judul dari pengaturan. |
| `{{totalEntries}}` | `7` | Total jumlah orang dalam antrian. |
| `{{hiddenCount}}` | `2` | Jumlah entri yang gak ditampilin karena kelebihan limit (`displayedQueueLimit` di pengaturan). |
| `{{showFields}}` | `"true"` atau `"false"` (string) | Status apakah custom field (misal alamat, catatan) ditampilkan. |
| `{{visibleEntries}}` | `[{"index":1,"id":"1","name":"BudiGaming","picture":"https://example.com/profile.jpg","badges":"VIP,TopDonator","badgeItems":[{"url":"https://tako.id/images/badges/verified-creator.png","title":"Verified Creator"}],"isVerified":true,"amount":15000,"formattedAmount":"Rp15.000","fields":[{"label":"Alamat","value":"Jl. Merdeka 1"}]}, ...]` | Daftar antrian yang **sudah** dipotong sesuai limit tampilan (JSON) — dipake buat render. Tiap entri punya `badgeItems`: array badge asli (ikon + judul), ini yang dipake buat nampilin badge sebagai **gambar**, bukan cuma teks. |
| `{{entries}}` / `{{queue}}` | sama struktur kayak `visibleEntries` | Daftar **semua** antrian (JSON), belum dipotong limit. |
| `{{fields}}` | `[{"label":"Alamat","value":"Jl. Merdeka 1"}, ...]` | Semua custom field dari semua entri, digabung jadi satu array datar (JSON). |

**Contoh Kode:**

```html
<html>
<head>
  <style>
    body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
    .ticket {
      position: relative;
      width: 100%;
      box-sizing: border-box;
      overflow: hidden;
      background: #f2e9d3;
      border: 2px solid #1c1712;
      box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
      padding: 0.9rem 1rem 0.9rem 1.5rem;
    }
    .tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
    .grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
    .row-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
    .title { font-size: 1.0625rem; font-weight: 700; }
    .count-badge { font-size: 0.875rem; font-weight: 700; color: #b6842c; border: 1px solid #7a7160; padding: 0.05rem 0.45rem; border-radius: 999px; flex-shrink: 0; }
    .perf { border-bottom: 1px dashed #7a7160; margin: 0.6rem 0; }
    .queue-list { display: flex; flex-direction: column; gap: 0.5rem; }
    .queue-row { display: flex; align-items: center; gap: 0.5rem; }
    .queue-num { width: 1.5rem; height: 1.5rem; border-radius: 50%; background: #e8dab8; border: 1px solid #1c1712; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 700; }
    .queue-avatar { width: 1.6rem; height: 1.6rem; border-radius: 50%; border: 1px solid #1c1712; object-fit: cover; flex-shrink: 0; display: none; }
    .queue-name { flex: 1; min-width: 0; font-size: 1rem; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .queue-badges { display: flex; align-items: center; gap: 0.2rem; flex-shrink: 0; }
    .queue-badge-icon { height: 1.0625rem; width: auto; display: block; }
    .queue-amount { font-size: 0.875rem; font-weight: 700; color: #7a7160; flex-shrink: 0; }
    .queue-fields { font-size: 0.75rem; color: #7a7160; padding-left: 2.1rem; }
    .queue-empty { font-size: 1rem; font-style: italic; color: #7a7160; }
    .hidden-note { font-size: 0.8125rem; text-align: center; color: #7a7160; font-weight: 700; margin-top: 0.3rem; }
  </style>
</head>
<body>
  <div class="ticket">
    <div class="tab"></div>
    <div class="grommet"></div>

    <div class="row-head">
      <div class="title" id="queue-title">{{title}}</div>
      <div class="count-badge" id="count-badge">{{totalEntries}}</div>
    </div>
    <div class="perf"></div>
    <div class="queue-list" id="queue-list"></div>
    <div class="hidden-note" id="hidden-note"></div>
  </div>

  <script>
    const entries = {{visibleEntries}};
    const hiddenCount = {{hiddenCount}};
    const showFields = "{{showFields}}" === "true";

    const list = document.getElementById("queue-list");
    const hiddenNote = document.getElementById("hidden-note");

    if (entries.length === 0) {
      const empty = document.createElement("p");
      empty.className = "queue-empty";
      empty.textContent = "Belum ada antrian.";
      list.appendChild(empty);
    }

    entries.forEach((entry) => {
      const row = document.createElement("div");
      row.className = "queue-row";

      const num = document.createElement("div");
      num.className = "queue-num";
      num.textContent = entry.index;

      const avatar = document.createElement("img");
      avatar.className = "queue-avatar";
      if (entry.picture) {
        avatar.src = entry.picture;
        avatar.style.display = "block";
        avatar.onerror = () => { avatar.style.display = "none"; };
      }

      const name = document.createElement("span");
      name.className = "queue-name";
      name.textContent = entry.name + (entry.isVerified ? " ✓" : "");

      const badgesEl = document.createElement("div");
      badgesEl.className = "queue-badges";
      (entry.badgeItems || []).forEach((badge) => {
        if (!badge.url) return;
        const icon = document.createElement("img");
        icon.className = "queue-badge-icon";
        icon.src = badge.url;
        icon.alt = badge.title || "";
        icon.title = badge.title || "";
        badgesEl.appendChild(icon);
      });

      const amount = document.createElement("span");
      amount.className = "queue-amount";
      amount.textContent = entry.formattedAmount;

      row.appendChild(num);
      row.appendChild(avatar);
      row.appendChild(name);
      row.appendChild(badgesEl);
      row.appendChild(amount);
      list.appendChild(row);

      if (showFields && entry.fields && entry.fields.length > 0) {
        const fieldsRow = document.createElement("div");
        fieldsRow.className = "queue-fields";
        fieldsRow.textContent = entry.fields.map((f) => f.label + ": " + (f.value || "-")).join("  •  ");
        list.appendChild(fieldsRow);
      }
    });

    if (hiddenCount > 0) {
      hiddenNote.textContent = "+" + hiddenCount + " lainnya menunggu";
    }
  </script>
</body>
</html>
```

---

### 9. Song Share

| Nama Variabel | Contoh Info | Keterangan |
| ---- |
| `{{title}}` / `{{songTitle}}` | `Judul Lagu` | Judul lagu yang sedang diputar. |
| `{{songArtist}}` | `Nama Artis` | Nama artis lagu yang sedang diputar. |
| `{{songCoverArtUrl}}` | https://example.com/cover.jpg | Link cover art lagu yang sedang diputar. |
| `{{requesterName}}` | `BudiGaming` | Nama pe-request lagu. |
| `{{requesterPicture}}` | https://example.com/profile.jpg | Link foto profil pe-request. |
| `{{requesterBadges}}` | `VIP,TopDonator` | Daftar *badge* pe-request (nama doang, dipisah koma) — **gak ada URL ikon** dari sistem buat baris "Now Playing" ini. Isi manual di `BADGE_ICON_URLS` dalam script kalo mau tampil sebagai gambar — kode di bawah udah diisiin 6 badge sistem resmi Tako, liat tabelnya di bagian Alert (§1). |
| `{{isRequesterVerified}}` | `true` atau `false` | Status verifikasi pe-request. |
| `{{duration}}` | `215` (dalam detik) | Durasi lagu (angka mentah). |
| `{{formattedDuration}}` | `03:35` | Durasi lagu yang sudah diformat. |
| `{{elapsed}}` | `42` (dalam detik) | Waktu lagu yang sudah berjalan (angka mentah). |
| `{{formattedElapsed}}` | `00:42` | Waktu lagu yang sudah berjalan, diformat. |
| `{{progressPercent}}` | `19` | Persentase progres lagu (0-100) — pakai buat bikin progress bar sendiri. |
| `{{isPlaying}}` | `true` atau `false` | Status lagu sedang diputar. |
| `{{isPaused}}` | `true` atau `false` | Status lagu sedang di-pause. |
| `{{isInGap}}` | `true` atau `false` | Status sedang dalam jeda antar lagu. |
| `{{gapRemaining}}` | `3` (dalam detik) | Sisa waktu jeda antar lagu. |
| `{{gapText}}` | `Lagu Berikutnya Pada 00:03` | Teks status jeda antar lagu. |
| `{{error}}` | `Lagu gagal diputar` | Pesan error jika ada, kosong jika tidak ada. |
| `{{nowPlaying}}` | `{"id":"1","title":"Judul Lagu","artist":"Nama Artis","coverArtUrl":"...","duration":215,"formattedDuration":"03:35","sender":{"name":"BudiGaming"},...}` | Objek (JSON) lagu yang sedang diputar, `null` jika tidak ada. |
| `{{queue}}` / `{{queues}}` | `[{"id":"2","title":"Lagu Antrian","artist":"Artis Lain","duration":180,"senderName":"Pendukung Lain","senderBadges":"VIP,TopDonator","senderBadgeItems":[{"url":"https://tako.id/images/badges/verified-creator.png","title":"Verified Creator"}],...}, ...]` | Daftar antrian lagu berikutnya (JSON). Tiap item punya `senderBadgeItems` — array badge asli (ikon + judul) buat requester lagu itu, ini yang beneran bisa dirender sebagai **gambar**. |

**Contoh Kode:**

```html
<html>
<head>
  <style>
    body { margin: 0; background: none; font-family: "Courier New", monospace; color: #1c1712; }
    .ticket {
      position: relative;
      width: 100%;
      box-sizing: border-box;
      overflow: hidden;
      background: #f2e9d3;
      border: 2px solid #1c1712;
      box-shadow: 0.2rem 0.25rem 0 0 #1c1712;
      padding: 0.9rem 1rem 0.9rem 1.5rem;
    }
    .tab { position: absolute; left: 0; top: 0; bottom: 0; width: 0.55rem; background: #9c3b2e; border-right: 2px solid #1c1712; }
    .grommet { position: absolute; left: 0.275rem; top: 0.85rem; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: #f2e9d3; border: 1px solid #1c1712; transform: translate(-50%, 0); z-index: 2; }
    .row-head { display: flex; justify-content: flex-end; margin-bottom: 0.5rem; }
    .status { font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: #7a7160; border: 1px solid #7a7160; padding: 0.1rem 0.4rem; }
    .error-banner { font-size: 0.8125rem; background: #9c3b2e; color: #f2e9d3; padding: 0.35rem 0.5rem; margin-bottom: 0.5rem; display: none; }
    .gap-banner { font-size: 0.9375rem; color: #7a7160; padding: 0.4rem 0; display: none; }
    .now-playing { display: flex; gap: 0.75rem; }
    .cover { position: relative; width: 3.4rem; height: 3.4rem; flex-shrink: 0; background: #e8dab8; border: 2px solid #1c1712; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; color: #7a7160; }
    .cover img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: none; }
    .info { flex: 1; min-width: 0; }
    .title { font-size: 1.0625rem; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .title.idle { font-weight: 400; color: #7a7160; }
    .artist { font-size: 0.9375rem; color: #7a7160; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 0.35rem; }
    .requester { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; font-size: 0.8125rem; color: #7a7160; margin-bottom: 0.45rem; }
    .requester img { width: 1.1rem; height: 1.1rem; border-radius: 50%; flex-shrink: 0; display: none; }
    .requester .badge-tag { font-size: 0.75rem; border: 1px solid #7a7160; padding: 0.05rem 0.35rem; }
    .requester .badge-icon { height: 0.9375rem; width: auto; display: block; }
    .progress-row { display: flex; align-items: center; gap: 0.5rem; }
    .bar-bg { flex: 1; height: 0.45rem; background: #e8dab8; border: 1px solid #1c1712; }
    .bar-fill { height: 100%; background: #b6842c; }
    .time { font-size: 0.8125rem; color: #7a7160; flex-shrink: 0; }
    .perf { border-bottom: 1px dashed #1c1712; margin: 0.65rem 0 0.5rem; }
    .setlist-label { font-size: 0.75rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: #7a7160; margin-bottom: 0.4rem; }
    .setlist { display: none; }
    .setlist-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9375rem; padding: 0.25rem 0; }
    .setlist-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .setlist-badges { display: flex; align-items: center; gap: 0.2rem; flex-shrink: 0; }
    .setlist-badge-icon { height: 0.9375rem; width: auto; display: block; }
    .setlist-time { color: #7a7160; flex-shrink: 0; }
  </style>
</head>
<body>
  <div class="ticket">
    <div class="tab"></div>
    <div class="grommet"></div>

    <div class="row-head">
      <span class="status" id="status-el">Now Playing</span>
    </div>

    <div class="error-banner" id="error-banner"></div>
    <div class="gap-banner" id="gap-banner"></div>

    <div id="player-body">
      <div class="now-playing">
        <div class="cover" id="cover">
          <span id="cover-fallback">♪</span>
          <img id="cover-img" alt="">
        </div>

        <div class="info">
          <div class="title" id="title-el">{{songTitle}}</div>
          <div class="artist" id="artist-el">{{songArtist}}</div>

          <div class="requester" id="requester-row">
            <img id="requester-pic" alt="">
            <span id="requester-name">{{requesterName}}</span>
            <span id="requester-verified"></span>
            <span id="requester-badges"></span>
          </div>

          <div class="progress-row" id="progress-row">
            <div class="bar-bg">
              <div class="bar-fill" style="width: {{progressPercent}}%"></div>
            </div>
            <div class="time" id="time-el" data-elapsed="{{elapsed}}" data-duration="{{duration}}">{{formattedElapsed}} / {{formattedDuration}}</div>
          </div>
        </div>
      </div>
    </div>

    <div id="setlist-wrap">
      <div class="perf" id="setlist-perf" style="display:none"></div>
      <div class="setlist" id="setlist">
        <div class="setlist-label" id="setlist-label">Up Next</div>
        <div id="setlist-body"></div>
      </div>
    </div>
  </div>

  <script>
    const isPlaying = "{{isPlaying}}" === "true";
    const isPaused = "{{isPaused}}" === "true";
    const isInGap = "{{isInGap}}" === "true";
    const gapText = "{{gapText}}";
    const errorText = "{{error}}";
    const coverUrl = "{{songCoverArtUrl}}".trim();
    const requesterPicture = "{{requesterPicture}}".trim();
    const requesterBadges = "{{requesterBadges}}".trim();
    const isRequesterVerified = "{{isRequesterVerified}}" === "true";

    const BADGE_ICON_URLS = {
      "Tako Verified Partner": "https://tako.id/images/badges/partnered-creator.png",
      "Tako Verified Creator": "https://tako.id/images/badges/verified-creator.png",
      "Rich Rich Club": "https://tako.id/images/badges/king.png",
      "Charity": "https://tako.id/images/badges/charity.png",
      "VTuber": "https://tako.id/images/badges/vtuber.png",
      "Tako Team": "https://tako.id/images/badges/admin.png"
    };

    const titleEl = document.getElementById("title-el");
    const artistEl = document.getElementById("artist-el");
    const coverImg = document.getElementById("cover-img");
    const coverFallback = document.getElementById("cover-fallback");
    const statusEl = document.getElementById("status-el");
    const errorBanner = document.getElementById("error-banner");
    const gapBanner = document.getElementById("gap-banner");
    const playerBody = document.getElementById("player-body");
    const requesterRow = document.getElementById("requester-row");
    const requesterPic = document.getElementById("requester-pic");
    const requesterVerified = document.getElementById("requester-verified");
    const requesterBadgesEl = document.getElementById("requester-badges");
    const progressRow = document.getElementById("progress-row");
    const setlistPerf = document.getElementById("setlist-perf");
    const setlist = document.getElementById("setlist");
    const setlistLabel = document.getElementById("setlist-label");
    const setlistBody = document.getElementById("setlist-body");

    const titleText = titleEl.textContent.trim();
    const isIdle = !titleText || titleText === "{{" + "songTitle" + "}}";

    if (errorText && !errorText.includes("{{")) {
      errorBanner.textContent = "⚠ " + errorText;
      errorBanner.style.display = "block";
    }

    if (isInGap) {
      playerBody.style.display = "none";
      gapBanner.textContent = gapText || "Menyiapkan lagu berikutnya...";
      gapBanner.style.display = "block";
      statusEl.textContent = "Gap";
    } else if (isIdle) {
      titleEl.textContent = "Belum ada lagu diputar";
      titleEl.classList.add("idle");
      artistEl.textContent = "Kirim hadiah untuk request lagu";
      requesterRow.style.display = "none";
      progressRow.style.display = "none";
      coverFallback.style.display = "flex";
      statusEl.textContent = "Idle";
    } else {
      statusEl.textContent = isPaused ? "Paused" : isPlaying ? "Now Playing" : "Buffering";

      if (coverUrl && !coverUrl.includes("{{")) {
        coverImg.src = coverUrl;
        coverImg.style.display = "block";
        coverFallback.style.display = "none";
        coverImg.onerror = () => {
          coverImg.style.display = "none";
          coverFallback.style.display = "flex";
        };
      }

      if (requesterPicture && !requesterPicture.includes("{{")) {
        requesterPic.src = requesterPicture;
        requesterPic.style.display = "block";
      }

      if (isRequesterVerified) requesterVerified.textContent = "✓";

      if (requesterBadges) {
        requesterBadges.split(",").filter(Boolean).forEach((name) => {
          const iconUrl = BADGE_ICON_URLS[name];

          if (iconUrl) {
            const icon = document.createElement("img");
            icon.className = "badge-icon";
            icon.src = iconUrl;
            icon.alt = name;
            icon.title = name;
            requesterBadgesEl.appendChild(icon);
          } else {
            const tag = document.createElement("span");
            tag.className = "badge-tag";
            tag.textContent = name;
            requesterBadgesEl.appendChild(tag);
          }
        });
      }
    }

    try {
      const queueData = {{queue}};

      if (Array.isArray(queueData) && queueData.length > 0) {
        setlistPerf.style.display = "block";
        setlist.style.display = "block";
        setlistLabel.textContent = "Up Next (" + queueData.length + ")";

        queueData.forEach((song, i) => {
          const row = document.createElement("div");
          row.className = "setlist-row";

          const name = document.createElement("span");
          name.className = "setlist-name";
          name.textContent = (i + 1) + ". " + song.title + " — " + song.artist;

          const badgesEl = document.createElement("div");
          badgesEl.className = "setlist-badges";
          (song.senderBadgeItems || []).forEach((badge) => {
            if (!badge.url) return;
            const icon = document.createElement("img");
            icon.className = "setlist-badge-icon";
            icon.src = badge.url;
            icon.alt = badge.title || "";
            icon.title = badge.title || "";
            badgesEl.appendChild(icon);
          });

          const duration = document.createElement("span");
          duration.className = "setlist-time";
          duration.textContent = song.formattedDuration;

          row.appendChild(name);
          row.appendChild(badgesEl);
          row.appendChild(duration);
          setlistBody.appendChild(row);
        });
      }
    } catch (e) {}
  </script>
</body>
</html>
```

---

## Informasi Tambahan yang Mungkin Muncul

* **"⚠ Ini pesan uji coba"**: Muncul ketika overlay di tes atau *preview*.
* **"⛔ Dikirim oleh Admin"**: Muncul ketika hadiah dikirim oleh tim/admin Tako.

---

## Cara Mengimplementasikan/Membuat Custom Overlay

1. **Masuk Dashboard Kreator**
2. **Cari Menu "Overlays" di Bagian Kiri**
3. **Pilih Overlay** (contoh: Alert)
4. **Di bagian "Tema", ubah ke "Custom HTML"**.
5. Copy-paste kode yang udah kamu bikin ke kolom/textarea yang muncul.
6. **Cek Preview** pada bagian atas.

${youtube}[Cara Bikin Custom Overlay di Tako!](8PEOMWGBN4w)
