Greasy Fork is available in English.

Duolingo Auto-Super

Duolingo Super Manager - Create referral links, activate Super, manage accounts

インストールの前に、Greasy Forkは、このスクリプトにアンチ機能が含まれることをお知らせします。これはあなたではなく、スクリプトの作者の利益を目的としてます。

このスクリプトは支払うことでのみ完全な機能となります。Greasy Fork は支払いに関与しないので、価値のあるものを入手できるかを検証していませんし、返金のお手伝いもできないので注意してください。 スクリプト作者による説明: Requires a private key purchased via Discord for full access

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

Advertisement:

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Duolingo Auto-Super
// @namespace    http://tampermonkey.net/
// @version      3.4
// @description  Duolingo Super Manager - Create referral links, activate Super, manage accounts
// @author       MeowWoof
// @match        https://www.duolingo.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @connect      duolingo-super022.vercel.app
// @run-at       document-idle
// @license      MIT
// @antifeature payment  Requires a private key purchased via Discord for full access
// @icon         https://d35aaqx5ub95lt.cloudfront.net/vendor/38dc6a042b0de3f6aeb44ff2aa70de73.svg
// ==/UserScript==

(function () {
'use strict';

const SCRIPT_VERSION  = '3.4';
const API_BASE        = 'https://duolingo-super022.vercel.app';
const FREE_KEY        = 'DS-FREE-PUBLIC-KEY-2025';
const _COUP           = 'DUOBNBJUNE2026';
const DISCORD_URL     = 'https://discord.gg/ufBrcGemBH';
const CD_STORE        = 'duods_cd_expire';
const VER_CHECK_STORE = 'duods_ver_ts';
const POLL_INTERVAL   = 30000;

let _sess        = null;
let _keyData     = null;
let _cdTimer     = null;
let _pollTimer   = null;
let _hist        = [];
let _hidden      = false;
let _animLock    = false;
let _pgLock      = false;
let _curPg       = 'auth';
let _ntTimer;
let _currentType = 'free';
let _expireTimer = null;

function _isDark() {
    const h = document.documentElement;
    if (h.dataset.theme === 'dark') return true;
    if (h.dataset.theme === 'light') return false;
    const bg = window.getComputedStyle(document.body).backgroundColor;
    if (bg) {
        const m = bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
        if (m) {
            const l = 0.299 * +m[1] + 0.587 * +m[2] + 0.114 * +m[3];
            if (l < 80) return true;
            if (l > 160) return false;
        }
    }
    return window.matchMedia('(prefers-color-scheme: dark)').matches;
}

function _applyTheme() {
    const r = document.getElementById('ds-root');
    if (!r) return;
    _isDark() ? r.setAttribute('data-dark', '1') : r.removeAttribute('data-dark');
}

function _watchTheme() {
    _applyTheme();
    const obs = new MutationObserver(_applyTheme);
    obs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });
    obs.observe(document.body, { attributes: true, attributeFilter: ['data-theme', 'class'] });
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', _applyTheme);
    setInterval(_applyTheme, 5000);
}

function _req(path, method, body) {
    return new Promise(resolve => {
        const hdrs = _hdrs();
        if (!hdrs && path !== '/api/key/info' && path !== '/api/admin/version') return resolve({ error: 'Unauthorized' });
        const cfg = {
            method: method || 'POST',
            url: API_BASE + path,
            headers: hdrs || { 'Content-Type': 'application/json' },
            timeout: 90000,
            onload: r => { try { resolve(JSON.parse(r.responseText)); } catch { resolve({ error: 'Parse error' }); } },
            onerror: () => resolve({ error: 'Network error' }),
            ontimeout: () => resolve({ error: 'Timeout' })
        };
        if (body) cfg.data = JSON.stringify(body);
        GM_xmlhttpRequest(cfg);
    });
}

function _hdrs() {
    if (!_sess) return null;
    if (Date.now() > _sess.exp) { _sess = null; return null; }
    return { 'Content-Type': 'application/json', 'x-ds-key': _sess.key, 'x-ds-token': _sess.tok };
}

function _fetchKey(key) {
    return new Promise(resolve => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: `${API_BASE}/api/key/info?key=${encodeURIComponent(key)}`,
            headers: { 'Content-Type': 'application/json' },
            timeout: 30000,
            onload: r => {
                try {
                    const d = JSON.parse(r.responseText);
                    if (d.ok) {
                        _sess = { key, tok: d.session_token || _fnv(key), exp: Date.now() + (d.session_ttl_ms || 3600000) };
                    }
                    resolve(d);
                } catch { resolve({ error: 'Parse error' }); }
            },
            onerror: () => resolve({ error: 'Network error' }),
            ontimeout: () => resolve({ error: 'Timeout' })
        });
    });
}

function _checkVersion() {
    return new Promise(resolve => {
        GM_xmlhttpRequest({
            method: 'GET', url: `${API_BASE}/api/admin/version`,
            headers: { 'Content-Type': 'application/json' }, timeout: 15000,
            onload: r => { try { resolve(JSON.parse(r.responseText)); } catch { resolve(null); } },
            onerror: () => resolve(null), ontimeout: () => resolve(null)
        });
    });
}

function _fnv(s) {
    let h = 0x811c9dc5;
    for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = (h * 0x01000193) >>> 0; }
    return h.toString(16).padStart(8, '0') + Date.now().toString(36);
}

const _set = (k, v) => GM_setValue(k, v);
const _get = (k, d) => GM_getValue(k, d);

function _tsGMT7() {
    const now = new Date(Date.now() + 7 * 3600000);
    return `${String(now.getUTCHours()).padStart(2,'0')}:${String(now.getUTCMinutes()).padStart(2,'0')}:${String(now.getUTCSeconds()).padStart(2,'0')}`;
}

GM_addStyle(`
#ds-root {
    --c-bg:      #ffffff;
    --c-surface: #f7f7f7;
    --c-border:  #e5e5e5;
    --c-text:    #3c3c3c;
    --c-muted:   #afafaf;
    --c-blue:    #1cb0f3;
    --c-green:   #58cc02;
    --c-red:     #ff4b4b;
    --c-amber:   #ffc800;
    --c-purple:  #ce82ff;
    --c-shadow:  rgba(0,0,0,0.08);
}
#ds-root[data-dark] {
    --c-bg:      #131f24;
    --c-surface: #1a2e38;
    --c-border:  #2a404d;
    --c-text:    #ffffff;
    --c-muted:   #7a9aaa;
    --c-blue:    #1cb0f3;
    --c-green:   #58cc02;
    --c-red:     #ff4b4b;
    --c-amber:   #ffc800;
    --c-purple:  #ce82ff;
    --c-shadow:  rgba(0,0,0,0.40);
}
#ds-root * { box-sizing: border-box; }
#ds-root p, #ds-root span, #ds-root button,
#ds-root input, #ds-root label, #ds-root div {
    font-family: inherit !important;
}
#ds-root p, #ds-root span { margin: 0; padding: 0; }
#ds-root a { text-decoration: none; }
#ds-root svg { flex-shrink: 0; }

.ds-shell {
    position: fixed; right: 16px; bottom: 16px;
    z-index: 2147483647;
    display: flex; flex-direction: column;
    align-items: flex-end; gap: 8px;
}
@media (max-width: 699px) { .ds-shell { margin-bottom: 72px; } }

.ds-card {
    width: 300px;
    background: var(--c-bg);
    border: 2px solid var(--c-border);
    border-radius: 20px;
    box-shadow: 0 4px 0 var(--c-border), 0 8px 24px var(--c-shadow);
    overflow: hidden;
    max-height: 88vh;
    display: flex;
    flex-direction: column;
}

.ds-toggle {
    display: flex; align-items: center; gap: 6px;
    padding: 7px 13px; border-radius: 20px;
    border: 2px solid var(--c-border);
    border-bottom: 3px solid var(--c-border);
    background: var(--c-bg); cursor: pointer;
    transition: transform .08s, border-bottom-width .08s, box-shadow .08s;
    box-shadow: 0 2px 0 var(--c-border);
}
.ds-toggle:hover {
    transform: translateY(2px);
    border-bottom-width: 2px;
    box-shadow: none;
}
.ds-toggle:active {
    transform: translateY(3px);
    border-bottom-width: 1px;
    box-shadow: none;
}
.ds-toggle-label {
    font-size: 13px; font-weight: 800;
    color: var(--c-muted); user-select: none;
}

.ds-page { display: none; }
.ds-page.active {
    display: flex; flex-direction: column;
    overflow-y: auto; max-height: 88vh;
}
.ds-page.active::-webkit-scrollbar { width: 0; }

.ds-section { padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
.ds-divider { height: 2px; background: var(--c-border); flex-shrink: 0; }

.ds-header {
    display: flex; align-items: center; justify-content: space-between;
    padding: 12px 14px; border-bottom: 2px solid var(--c-border);
    flex-shrink: 0;
}
.ds-header-title { font-size: 14px; font-weight: 800; color: var(--c-text); }
.ds-header-sub   { font-size: 11px; font-weight: 600; color: var(--c-muted); margin-top: 1px; }

.ds-label { font-size: 10px; font-weight: 800; letter-spacing: .8px; text-transform: uppercase; color: var(--c-muted); }
.ds-small { font-size: 12px; font-weight: 600; color: var(--c-muted); line-height: 1.45; }
.ds-mono  { font-family: monospace !important; }

.ds-row     { display: flex; align-items: center; gap: 7px; }
.ds-col     { display: flex; flex-direction: column; gap: 7px; }
.ds-between { display: flex; align-items: center; justify-content: space-between; }

.ds-badge {
    display: inline-flex; align-items: center;
    padding: 3px 10px; border-radius: 99px;
    font-size: 10px; font-weight: 800; letter-spacing: .6px;
    user-select: none; flex-shrink: 0;
}
.ds-badge-free  { background: #d7ffb8; color: #3c7a00; }
.ds-badge-paid  { background: #ddf4ff; color: #0e7da8; }
.ds-badge-event { background: #fff0a0; color: #a06800; }
.ds-badge-admin { background: #f1e4ff; color: #7a3daf; }
#ds-root[data-dark] .ds-badge-free  { background: #1a3d00; color: #58cc02; }
#ds-root[data-dark] .ds-badge-paid  { background: #003d52; color: #1cb0f3; }
#ds-root[data-dark] .ds-badge-event { background: #ffc800; color: #3c2800; }
#ds-root[data-dark] .ds-badge-admin { background: #2e0052; color: #ce82ff; }

.ds-input-wrap {
    display: flex; align-items: center; gap: 8px;
    border: 2px solid var(--c-border); border-radius: 12px;
    padding: 0 12px; background: var(--c-surface); height: 42px;
    transition: border-color .15s;
}
.ds-input-wrap:focus-within { border-color: var(--c-blue); }
.ds-input {
    flex: 1; border: none; background: none; outline: none;
    font-size: 13px; font-weight: 700; color: var(--c-text);
}
.ds-input::placeholder { color: var(--c-muted); font-weight: 600; }

.ds-btn {
    display: flex; align-items: center; justify-content: center; gap: 6px;
    height: 42px; padding: 0 14px; border-radius: 12px;
    border: 2px solid transparent;
    border-bottom: 3px solid transparent;
    font-size: 14px; font-weight: 800;
    cursor: pointer; user-select: none;
    transition: transform .08s, border-bottom-width .08s, box-shadow .08s, filter .08s;
    position: relative;
}
.ds-btn:hover:not(:disabled) {
    transform: translateY(2px);
    border-bottom-width: 2px !important;
    box-shadow: none !important;
    filter: brightness(.97);
}
.ds-btn:active:not(:disabled) {
    transform: translateY(3px) !important;
    border-bottom-width: 1px !important;
    box-shadow: none !important;
    filter: brightness(.94) !important;
}
.ds-btn:disabled { opacity: .38; cursor: not-allowed; }

.ds-btn-primary {
    background: var(--c-blue); color: #fff;
    border-color: #0e86b8; border-bottom-color: #0a6a94;
    box-shadow: 0 2px 0 #0a6a94;
}
.ds-btn-outline {
    background: var(--c-bg);
    border-color: var(--c-border); border-bottom-color: #c8c8c8;
    color: var(--c-text);
    box-shadow: 0 2px 0 #c8c8c8;
}
.ds-btn-ghost {
    background: var(--c-surface);
    border-color: var(--c-border); border-bottom-color: #c8c8c8;
    color: var(--c-text);
    box-shadow: 0 2px 0 #c8c8c8;
}
.ds-btn-danger {
    background: transparent;
    border-color: var(--c-red); border-bottom-color: #c83030;
    color: var(--c-red);
    box-shadow: 0 2px 0 #c83030;
}
.ds-btn-green {
    background: var(--c-green); color: #fff;
    border-color: #3d8f00; border-bottom-color: #2d6b00;
    box-shadow: 0 2px 0 #2d6b00;
}
.ds-btn-purple {
    background: var(--c-purple); color: #fff;
    border-color: #9e52d4; border-bottom-color: #7a3aac;
    box-shadow: 0 2px 0 #7a3aac;
}
.ds-btn-full  { width: 100%; }
.ds-btn-sm    { height: 34px; font-size: 12px; padding: 0 10px; border-radius: 10px; }

#ds-root[data-dark] .ds-btn-outline { border-bottom-color: #1a3040; box-shadow: 0 2px 0 #1a3040; }
#ds-root[data-dark] .ds-btn-ghost   { border-bottom-color: #1a3040; box-shadow: 0 2px 0 #1a3040; }

.ds-progress-wrap {
    height: 8px; background: var(--c-surface);
    border-radius: 99px; overflow: hidden;
    border: 2px solid var(--c-border);
}
.ds-progress-fill {
    height: 100%; border-radius: 99px;
    background: var(--c-blue); transition: width .5s cubic-bezier(.34,1.56,.64,1);
}
.ds-progress-fill.warn { background: var(--c-red); }

.ds-log {
    background: var(--c-surface); border: 2px solid var(--c-border);
    border-radius: 12px; padding: 8px 10px;
    font-size: 11px; font-family: monospace !important;
    font-weight: 600; color: var(--c-muted);
    max-height: 130px; overflow-y: auto; line-height: 1.65;
    white-space: pre-wrap; word-break: break-all;
}
.ds-log::-webkit-scrollbar { width: 3px; }
.ds-log::-webkit-scrollbar-thumb { background: var(--c-border); border-radius: 3px; }
.lo { color: var(--c-green); }
.le { color: var(--c-red); }
.li { color: var(--c-blue); }
.lw { color: var(--c-amber); }

.ds-tabs {
    display: flex; border-bottom: 2px solid var(--c-border);
    padding: 0 6px; gap: 2px; flex-shrink: 0;
}
.ds-tab {
    flex: 1; padding: 8px 4px; border: none; background: none;
    font-size: 13px; font-weight: 800; color: var(--c-muted);
    cursor: pointer; border-bottom: 3px solid transparent;
    margin-bottom: -2px; transition: color .15s, border-color .15s;
}
.ds-tab.on { color: var(--c-blue); border-bottom-color: var(--c-blue); }
.ds-tab:hover:not(.on) { color: var(--c-text); }

.ds-pane        { display: none; }
.ds-pane.active { display: flex; flex-direction: column; }

.ds-cd {
    display: none; align-items: center; justify-content: space-between;
    padding: 8px 12px;
    background: #fff8e0; border: 2px solid #ffc800; border-radius: 10px;
}
#ds-root[data-dark] .ds-cd { background: #2a2200; border-color: #a06800; }
.ds-cd.on { display: flex; animation: ds-slidein .2s cubic-bezier(.34,1.56,.64,1); }
.ds-cd-label { font-size: 11px; font-weight: 800; color: #a06800; display: flex; align-items: center; gap: 4px; }
#ds-root[data-dark] .ds-cd-label { color: var(--c-amber); }
.ds-cd-time { font-size: 13px; font-weight: 800; color: #a06800; font-family: monospace !important; }
#ds-root[data-dark] .ds-cd-time { color: var(--c-amber); }

.ds-err { font-size: 11px; font-weight: 700; color: var(--c-red); min-height: 14px; }

.ds-spinner { display: none; justify-content: center; align-items: center; gap: 4px; }
.ds-spinner.on { display: flex; }
.ds-dot {
    width: 5px; height: 5px; border-radius: 50%;
    background: var(--c-muted);
    animation: ds-pulse 1.1s ease-in-out infinite;
}
.ds-dot:nth-child(2) { animation-delay: .16s; }
.ds-dot:nth-child(3) { animation-delay: .32s; }
@keyframes ds-pulse {
    0%, 80%, 100% { opacity: .25; transform: scale(.6); }
    40%            { opacity: 1;   transform: scale(1); }
}
@keyframes ds-slidein {
    from { opacity: 0; transform: translateY(-4px) scale(.97); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes ds-popin {
    from { opacity: 0; transform: scale(.93); }
    to   { opacity: 1; transform: scale(1); }
}

.ds-copy-tag {
    display: inline-flex; align-items: center;
    padding: 1px 6px; border-radius: 6px; margin-left: 4px;
    font-size: 10px; font-weight: 800; cursor: pointer;
    background: var(--c-surface);
    border: 2px solid var(--c-border);
    color: var(--c-muted);
    transition: transform .08s, box-shadow .08s;
    vertical-align: middle;
    box-shadow: 0 2px 0 var(--c-border);
}
.ds-copy-tag:hover { transform: translateY(1px); box-shadow: none; }
.ds-copy-tag:active { transform: translateY(2px); box-shadow: none; }

.ds-ntf-shell {
    position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
    z-index: 2147483647; pointer-events: none;
}
.ds-ntf-box {
    background: var(--c-bg);
    border: 2px solid var(--c-border);
    border-bottom: 4px solid var(--c-border);
    border-radius: 16px; padding: 10px 14px;
    box-shadow: 0 4px 0 var(--c-border), 0 8px 24px var(--c-shadow);
    opacity: 0; transform: translateY(10px) scale(.96); pointer-events: auto;
    transition: opacity .22s cubic-bezier(.34,1.56,.64,1), transform .22s cubic-bezier(.34,1.56,.64,1);
    min-width: 200px; max-width: 280px;
}
.ds-ntf-box.show { opacity: 1; transform: translateY(0) scale(1); }
.ds-ntf-title { font-size: 13px; font-weight: 800; color: var(--c-text); }
.ds-ntf-body  { font-size: 11px; font-weight: 600; color: var(--c-muted); margin-top: 2px; }

.ds-key-display {
    font-size: 12px; font-weight: 800; color: var(--c-text);
    font-family: monospace !important;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.ds-expiry {
    display: none; align-items: center; gap: 5px;
    padding: 7px 11px;
    background: #fff8e0; border: 2px solid #ffc800; border-radius: 10px;
    animation: ds-slidein .2s cubic-bezier(.34,1.56,.64,1);
}
#ds-root[data-dark] .ds-expiry { background: #2a2200; border-color: #a06800; }
.ds-expiry.on { display: flex; }
.ds-expiry-txt { font-size: 11px; font-weight: 800; color: #a06800; }
#ds-root[data-dark] .ds-expiry-txt { color: var(--c-amber); }

.ds-quota-row { display: flex; flex-direction: column; gap: 5px; }
`);

const _root = document.createElement('div');
_root.id = 'ds-root';
_root.innerHTML = `
<div class="ds-ntf-shell" id="ds-ntf">
    <div class="ds-ntf-box" id="ds-ntf-box">
        <p class="ds-ntf-title" id="ds-ntf-title"></p>
        <p class="ds-ntf-body"  id="ds-ntf-body"></p>
    </div>
</div>

<div class="ds-shell" id="ds-shell">

    <button class="ds-toggle" id="ds-toggle">
        <svg id="ds-ico-show" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="color:var(--c-muted);display:none;">
            <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
            <circle cx="12" cy="12" r="3"/>
        </svg>
        <svg id="ds-ico-hide" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="color:var(--c-muted);">
            <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
            <line x1="1" y1="1" x2="23" y2="23"/>
        </svg>
        <span class="ds-toggle-label" id="ds-toggle-label">Hide</span>
    </button>

    <div class="ds-card" id="ds-card">

        <!-- AUTH PAGE -->
        <div class="ds-page active" id="ds-page-auth">
            <div class="ds-header">
                <div>
                    <p class="ds-header-title">DuoDS</p>
                    <p class="ds-header-sub">Enter your key to continue</p>
                </div>
                <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--c-muted)" stroke-width="1.5">
                    <rect x="3" y="11" width="18" height="11" rx="2"/>
                    <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
                </svg>
            </div>
            <div class="ds-section ds-col" style="gap:8px;">
                <div class="ds-row">
                    <div class="ds-input-wrap" style="flex:1;">
                        <input id="ds-key-in" class="ds-input ds-mono" type="password" placeholder="DS-XXXXXXXXXXXXXXXX" autocomplete="off">
                    </div>
                    <button class="ds-btn ds-btn-primary" id="ds-unlock-btn" style="flex-shrink:0;">Unlock</button>
                </div>
                <div class="ds-spinner" id="ds-auth-spin"><div class="ds-dot"></div><div class="ds-dot"></div><div class="ds-dot"></div></div>
                <p class="ds-err" id="ds-auth-err"></p>
                <button class="ds-btn ds-btn-green ds-btn-full" id="ds-free-btn">Use Free Tier</button>
                <div class="ds-divider"></div>
                <div class="ds-between">
                    <span class="ds-small">Need a key?</span>
                    <a href="${DISCORD_URL}" target="_blank" rel="noopener" class="ds-btn ds-btn-sm ds-btn-outline">Discord →</a>
                </div>
                <div class="ds-between" style="padding-top:2px;">
                    <span class="ds-small" style="color:var(--c-border)">discord.gg/ufBrcGemBH</span>
                    <span class="ds-small" style="color:var(--c-border)">v${SCRIPT_VERSION}</span>
                </div>
            </div>
        </div>

        <!-- DASH PAGE -->
        <div class="ds-page" id="ds-page-dash">
            <div class="ds-header">
                <div style="min-width:0;flex:1;">
                    <div class="ds-row" style="gap:6px;">
                        <p class="ds-key-display" id="ds-key-disp">DS-XXXX...</p>
                        <span class="ds-badge ds-badge-free" id="ds-tier-tag" style="display:none;">FREE</span>
                    </div>
                    <p class="ds-small" id="ds-key-meta" style="margin-top:2px;">Batch: ? · CD: ?s</p>
                </div>
                <button class="ds-btn ds-btn-sm ds-btn-danger" id="ds-chkey-btn">Change</button>
            </div>

            <div class="ds-section ds-col" style="gap:6px;padding-bottom:8px;">
                <div class="ds-expiry" id="ds-expiry">
                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--c-amber)" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/></svg>
                    <span class="ds-expiry-txt" id="ds-expiry-txt">Expires in --:--</span>
                </div>
                <div class="ds-quota-row" id="ds-quota-wrap">
                    <div class="ds-between">
                        <span class="ds-label">Daily quota</span>
                        <span class="ds-small ds-mono" id="ds-quota-val">0 / 0</span>
                    </div>
                    <div class="ds-progress-wrap">
                        <div class="ds-progress-fill" id="ds-quota-fill" style="width:0%"></div>
                    </div>
                </div>
                <div class="ds-cd" id="ds-cd">
                    <span class="ds-cd-label">
                        <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/></svg>
                        Cooldown
                    </span>
                    <span class="ds-cd-time" id="ds-cd-time">00:00</span>
                </div>
            </div>
            <div class="ds-divider"></div>

            <div class="ds-tabs">
                <button class="ds-tab on" data-tab="link">Links</button>
                <button class="ds-tab"    data-tab="active">Activate</button>
                <button class="ds-tab"    data-tab="hist">History</button>
            </div>

            <div class="ds-pane active ds-section ds-col" id="ds-pane-link" style="gap:8px;">
                <p class="ds-small">
                    Create <span id="ds-batch-n" style="font-weight:700;color:var(--c-text);">?</span> links per batch.
                </p>
                <button class="ds-btn ds-btn-primary ds-btn-full" id="ds-create-btn">
                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
                    <span id="ds-create-lbl">Create Batch</span>
                </button>
                <div class="ds-progress-wrap" id="ds-create-prog" style="display:none;">
                    <div class="ds-progress-fill" id="ds-create-fill"></div>
                </div>
                <div class="ds-log" id="ds-log-link">Ready...</div>
            </div>

            <div class="ds-pane ds-section ds-col" id="ds-pane-active" style="gap:8px;">
                <button class="ds-btn ds-btn-primary ds-btn-full" id="ds-activate-btn">
                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
                    <span id="ds-activate-lbl">Activate Super (Family)</span>
                </button>
                <button class="ds-btn ds-btn-purple ds-btn-full" id="ds-coupon-btn">
                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 12V22H4V12"/><path d="M22 7H2v5h20V7z"/><path d="M12 22V7"/></svg>
                    <span id="ds-coupon-lbl">Get 1 Month Free</span>
                </button>
                <div class="ds-log" id="ds-log-active">Waiting...</div>
            </div>

            <div class="ds-pane ds-section ds-col" id="ds-pane-hist" style="gap:8px;">
                <div class="ds-row">
                    <button class="ds-btn ds-btn-ghost ds-btn-full" id="ds-loadhist-btn">
                        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-3.68"/></svg>
                        <span id="ds-loadhist-lbl">Load</span>
                    </button>
                    <button class="ds-btn ds-btn-outline ds-btn-sm" id="ds-copyall-btn">Copy all</button>
                </div>
                <div class="ds-log" id="ds-hist-log" style="max-height:180px;">Press load...</div>
            </div>

            <div class="ds-divider"></div>
            <div class="ds-section ds-between" style="padding-top:8px;padding-bottom:10px;">
                <span class="ds-small" style="color:var(--c-border)">discord.gg/ufBrcGemBH</span>
                <span class="ds-small" style="color:var(--c-border)">v${SCRIPT_VERSION}</span>
            </div>
        </div>

        <!-- CHANGE KEY PAGE -->
        <div class="ds-page" id="ds-page-chkey">
            <div class="ds-header">
                <div>
                    <p class="ds-header-title">Switch Key</p>
                    <p class="ds-header-sub">Enter new key or go back to free tier</p>
                </div>
            </div>
            <div class="ds-section ds-col" style="gap:8px;">
                <div class="ds-row">
                    <div class="ds-input-wrap" style="flex:1;">
                        <input id="ds-newkey-in" class="ds-input ds-mono" type="password" placeholder="DS-XXXXXXXXXXXXXXXX" autocomplete="off">
                    </div>
                    <button class="ds-btn ds-btn-primary" id="ds-newkey-btn" style="flex-shrink:0;">Apply</button>
                </div>
                <div class="ds-spinner" id="ds-chkey-spin"><div class="ds-dot"></div><div class="ds-dot"></div><div class="ds-dot"></div></div>
                <p class="ds-err" id="ds-chkey-err"></p>
                <button class="ds-btn ds-btn-green ds-btn-full" id="ds-activefree-btn">Use Free Tier</button>
                <div class="ds-divider"></div>
                <div class="ds-between">
                    <span class="ds-small">Get a premium key</span>
                    <a href="${DISCORD_URL}" target="_blank" rel="noopener" class="ds-btn ds-btn-sm ds-btn-outline">Discord →</a>
                </div>
                <button class="ds-btn ds-btn-outline ds-btn-full" id="ds-back-btn">
                    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
                    Back
                </button>
                <div class="ds-between" style="padding-top:2px;">
                    <span class="ds-small" style="color:var(--c-border)">discord.gg/ufBrcGemBH</span>
                    <span class="ds-small" style="color:var(--c-border)">v${SCRIPT_VERSION}</span>
                </div>
            </div>
        </div>

        <!-- BLOCKED PAGE -->
        <div class="ds-page" id="ds-page-blocked">
            <div class="ds-header">
                <div>
                    <p class="ds-header-title" style="color:var(--c-red);">Key In Use</p>
                    <p class="ds-header-sub">This key is active in another session</p>
                </div>
                <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--c-red)" stroke-width="1.5">
                    <rect x="3" y="11" width="18" height="11" rx="2"/>
                    <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
                    <line x1="9" y1="15" x2="15" y2="19"/><line x1="15" y1="15" x2="9" y2="19"/>
                </svg>
            </div>
            <div class="ds-section ds-col" style="gap:8px;">
                <p class="ds-small">Use a different key or contact Discord to get your own private key.</p>
                <button class="ds-btn ds-btn-danger ds-btn-full" id="ds-blocked-chkey-btn">Change Key</button>
                <div class="ds-divider"></div>
                <div class="ds-between">
                    <span class="ds-small">Need support?</span>
                    <a href="${DISCORD_URL}" target="_blank" rel="noopener" class="ds-btn ds-btn-sm ds-btn-outline">Join Discord</a>
                </div>
                <div class="ds-between" style="padding-top:2px;">
                    <span class="ds-small" style="color:var(--c-border)">discord.gg/ufBrcGemBH</span>
                    <span class="ds-small" style="color:var(--c-border)">v${SCRIPT_VERSION}</span>
                </div>
            </div>
        </div>

        <!-- UPDATE PAGE -->
        <div class="ds-page" id="ds-page-update">
            <div class="ds-header">
                <div>
                    <p class="ds-header-title" style="color:var(--c-amber);">Update Required</p>
                    <p class="ds-header-sub">Your version is outdated</p>
                </div>
                <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--c-amber)" stroke-width="1.5">
                    <circle cx="12" cy="12" r="9"/>
                    <path d="M12 7v6"/><circle cx="12" cy="16.5" r="1" fill="var(--c-amber)"/>
                </svg>
            </div>
            <div class="ds-section ds-col" style="gap:8px;">
                <p class="ds-small" id="ds-update-msg">Script is outdated. Please update to the latest version to continue.</p>
                <a class="ds-btn ds-btn-primary ds-btn-full" id="ds-update-btn" href="#" target="_blank" rel="noopener" style="text-decoration:none;">Update Now</a>
                <div class="ds-between" style="padding-top:2px;">
                    <span class="ds-small" style="color:var(--c-border)">Current: v${SCRIPT_VERSION}</span>
                </div>
            </div>
        </div>

    </div>
</div>
`;
document.body.appendChild(_root);
_watchTheme();

function _notify(title, body, dur = 5) {
    const box = document.getElementById('ds-ntf-box');
    document.getElementById('ds-ntf-title').textContent = title;
    document.getElementById('ds-ntf-body').textContent  = body;
    box.classList.add('show');
    clearTimeout(_ntTimer);
    _ntTimer = setTimeout(() => box.classList.remove('show'), dur * 1000);
}

function _toggleVis(hide) {
    if (_animLock) return;
    _animLock = true;
    _hidden   = hide;
    const card  = document.getElementById('ds-card');
    const shell = document.getElementById('ds-shell');
    const lbl   = document.getElementById('ds-toggle-label');
    const icoH  = document.getElementById('ds-ico-hide');
    const icoS  = document.getElementById('ds-ico-show');
    const h     = card.getBoundingClientRect().height || 300;
    shell.style.transition = card.style.transition = '.55s cubic-bezier(.16,1,.32,1)';
    if (hide) {
        shell.style.bottom = `-${h - 6}px`;
        card.style.opacity = '0';
        lbl.textContent    = 'Show';
        icoH.style.display = 'none';
        icoS.style.display = '';
    } else {
        shell.style.bottom = '14px';
        card.style.opacity = '';
        lbl.textContent    = 'Hide';
        icoH.style.display = '';
        icoS.style.display = 'none';
    }
    setTimeout(() => { shell.style.transition = card.style.transition = ''; _animLock = false; }, 560);
}

document.getElementById('ds-toggle').addEventListener('click', () => _toggleVis(!_hidden));

function _goPage(to) {
    if (_pgLock || _curPg === to) return;
    _pgLock = true;
    const fromEl = document.getElementById(`ds-page-${_curPg}`);
    const toEl   = document.getElementById(`ds-page-${to}`);
    if (!fromEl || !toEl) { _pgLock = false; return; }
    const card = document.getElementById('ds-card');
    const oldH = card.offsetHeight;
    toEl.style.cssText = `display:flex;flex-direction:column;position:absolute;visibility:hidden;pointer-events:none;width:${card.clientWidth}px;`;
    card.appendChild(toEl);
    const newH = toEl.scrollHeight;
    toEl.style.cssText = '';
    card.style.height     = oldH + 'px';
    card.style.transition = 'height .4s cubic-bezier(.16,1,.32,1)';
    fromEl.style.transition = 'opacity .2s';
    fromEl.style.opacity    = '0';
    requestAnimationFrame(() => { card.style.height = newH + 'px'; });
    setTimeout(() => {
        fromEl.classList.remove('active');
        fromEl.style.cssText = '';
        toEl.classList.add('active');
        _curPg = to;
        setTimeout(() => { card.style.height = ''; card.style.transition = ''; _pgLock = false; }, 420);
    }, 220);
}

document.querySelectorAll('#ds-root .ds-tab').forEach(btn => {
    btn.addEventListener('click', () => {
        document.querySelectorAll('#ds-root .ds-tab').forEach(b => b.classList.remove('on'));
        btn.classList.add('on');
        ['link', 'active', 'hist'].forEach(t => {
            const el = document.getElementById(`ds-pane-${t}`);
            if (el) el.classList.toggle('active', btn.dataset.tab === t);
        });
        if (btn.dataset.tab === 'hist' && _sess) {
            const key = _get('dds_key', '');
            if (key && !_hist.length) {
                _req(`/api/link/list?key=${encodeURIComponent(key)}`, 'GET').then(r => {
                    if (r.ok && r.links) { _hist = r.links; _renderHist(); }
                });
            }
        }
    });
});

function _log(id, msg, type = 'i') {
    const el = document.getElementById(id);
    if (!el) return;
    const cls = { o: 'lo', e: 'le', i: 'li', w: 'lw' }[type] || 'li';
    const span = document.createElement('span');
    span.className   = cls;
    span.textContent = `[${_tsGMT7()}] ${msg}`;
    el.appendChild(span);
    el.appendChild(document.createTextNode('\n'));
    el.scrollTop = el.scrollHeight;
}

function _logClear(id) {
    const el = document.getElementById(id);
    if (el) el.textContent = '';
}

function _logErr(id, msg) {
    const el = document.getElementById(id);
    if (!el) return;
    el.textContent = '';
    const s = document.createElement('span');
    s.className   = 'le';
    s.textContent = msg;
    el.appendChild(s);
}

function _setBusy(btnId, spinId, on, label) {
    const btn  = document.getElementById(btnId);
    const spin = spinId ? document.getElementById(spinId) : null;
    if (!btn) return;
    btn.disabled = on;
    if (spin) spin.classList.toggle('on', on);
    const lbl = btn.querySelector('span[id]') || btn.querySelector('span');
    if (lbl && label) { if (on) { lbl._orig = lbl.textContent; lbl.textContent = label; } else if (lbl._orig) { lbl.textContent = lbl._orig; lbl._orig = null; } }
}

function _refreshQuota(used, limit) {
    const pct  = limit > 0 ? Math.min((used / limit) * 100, 100) : 0;
    const fill = document.getElementById('ds-quota-fill');
    const val  = document.getElementById('ds-quota-val');
    if (fill) { fill.style.width = pct + '%'; fill.classList.toggle('warn', pct > 85); }
    if (val)  val.textContent = `${used} / ${limit}`;
}

function _saveCD(sec) { if (sec > 0) _set(CD_STORE, Date.now() + sec * 1000); }
function _loadCD() {
    const exp = _get(CD_STORE, 0);
    if (!exp) return 0;
    const rem = Math.ceil((exp - Date.now()) / 1000);
    return rem > 0 ? rem : 0;
}

function _startCD(sec, save = true) {
    if (_cdTimer) clearInterval(_cdTimer);
    let rem = Math.ceil(sec);
    if (save) _saveCD(rem);
    const cdEl  = document.getElementById('ds-cd');
    const cdTx  = document.getElementById('ds-cd-time');
    const cBtn  = document.getElementById('ds-create-btn');
    if (cdEl) cdEl.classList.add('on');
    if (cBtn) cBtn.disabled = true;
    function tick() {
        if (rem <= 0) {
            clearInterval(_cdTimer);
            if (cdEl) cdEl.classList.remove('on');
            if (cBtn) cBtn.disabled = false;
            return;
        }
        if (cdTx) cdTx.textContent = `${String(Math.floor(rem/60)).padStart(2,'0')}:${String(rem%60).padStart(2,'0')}`;
        rem--;
    }
    tick();
    _cdTimer = setInterval(tick, 1000);
}

function _startExpireCountdown(seconds) {
    if (_expireTimer) clearInterval(_expireTimer);
    const banner = document.getElementById('ds-expiry');
    const txt    = document.getElementById('ds-expiry-txt');
    if (!banner || !txt) return;
    let rem = Math.ceil(seconds);
    if (rem <= 0 || rem > 3600) { banner.classList.remove('on'); return; }
    banner.classList.add('on');
    function tick() {
        if (rem <= 0) { clearInterval(_expireTimer); banner.classList.remove('on'); _onKeyExpired(); return; }
        const hh = Math.floor(rem/3600);
        const mm = String(Math.floor((rem%3600)/60)).padStart(2,'0');
        const ss = String(rem%60).padStart(2,'0');
        txt.textContent = hh > 0 ? `Expires in ${hh}:${mm}:${ss}` : `Expires in ${mm}:${ss}`;
        rem--;
    }
    tick();
    _expireTimer = setInterval(tick, 1000);
}

function _onKeyExpired() {
    if (_pollTimer)   { clearInterval(_pollTimer);   _pollTimer   = null; }
    if (_cdTimer)     { clearInterval(_cdTimer);     _cdTimer     = null; }
    if (_expireTimer) { clearInterval(_expireTimer); _expireTimer = null; }
    _sess = null; _keyData = null; _currentType = 'free';
    _set('dds_key', '');
    _notify('Key Expired', 'Switched to free tier.', 6);
    _fetchKey(FREE_KEY).then(r => {
        if (r.ok && _sess) { _set('dds_key', FREE_KEY); _applyKey(r, FREE_KEY); _goPage('dash'); _startPoll(FREE_KEY); }
        else { _sess = null; _goPage('auth'); }
    });
}

function _tierLabel(type) {
    return { free: 'FR', paid: 'PR', event: 'EV', admin: 'AD' }[type] || type.toUpperCase();
}

function _applyTierFx(newType) {
    const tag = document.getElementById('ds-tier-tag');
    if (!tag) return;
    tag.className     = `ds-badge ds-badge-${newType}`;
    tag.textContent   = _tierLabel(newType);
    tag.style.display = '';
}

function _applyKey(data, key) {
    _keyData     = data;
    _currentType = data.key_type || 'paid';
    const disp   = document.getElementById('ds-key-disp');
    const meta   = document.getElementById('ds-key-meta');
    const batchN = document.getElementById('ds-batch-n');
    if (disp)   disp.textContent   = key.slice(0, 16) + '...';
    if (batchN) batchN.textContent = data.batch_size;
    let expStr = '';
    if (data.expires_in_seconds != null) {
        const h = Math.floor(data.expires_in_seconds / 3600);
        const m = Math.floor((data.expires_in_seconds % 3600) / 60);
        expStr  = ` · Exp: ${h}h${m}m`;
        _startExpireCountdown(data.expires_in_seconds);
    } else {
        const banner = document.getElementById('ds-expiry');
        if (banner) banner.classList.remove('on');
        if (_expireTimer) { clearInterval(_expireTimer); _expireTimer = null; }
    }
    if (meta) meta.textContent = `Batch: ${data.batch_size} · CD: ${data.cd_seconds}s · ${data.used_total}/${data.total_limit}${expStr}`;
    _refreshQuota(data.used_today || 0, data.daily_limit);
    _applyTierFx(_currentType);
    const cdRemain = (data.cooldown_remain && data.cooldown_remain > 0) ? data.cooldown_remain : _loadCD();
    if (cdRemain > 0) _startCD(cdRemain, false);
}

function _startPoll(key) {
    if (_pollTimer) clearInterval(_pollTimer);
    _pollTimer = setInterval(async () => {
        if (!_sess) return;
        const r = await _fetchKey(key);
        if (!r.ok) return;
        if (r.in_use) {
            clearInterval(_pollTimer); _pollTimer = null;
            _sess = null; _goPage('blocked');
            _notify('Key In Use', 'Another session is using this key.', 8);
            return;
        }
        if (r.expires_in_seconds != null && r.expires_in_seconds <= 0) {
            clearInterval(_pollTimer); _pollTimer = null;
            _onKeyExpired(); return;
        }
        const savedKey = _get('dds_key', '');
        if (savedKey === FREE_KEY && r.key_type === 'event' && r.event_key) {
            _set('dds_key', r.event_key); _sess = null;
            const ev = await _fetchKey(r.event_key);
            if (ev.ok && _sess) { _applyKey(ev, r.event_key); _notify('Upgraded to EVENT', 'Free key upgraded to EVENT tier!', 6); }
            return;
        }
        if (r.key_type !== _currentType) {
            _applyKey(r, savedKey || key);
            _notify('Key Updated', `Tier: ${_tierLabel(r.key_type)}`, 4);
        } else if (r.expires_in_seconds != null) {
            _startExpireCountdown(r.expires_in_seconds);
        }
    }, POLL_INTERVAL);
}

async function _unlock(key, errId, spinId, btnId, lblId) {
    const errEl = document.getElementById(errId);
    if (!key) return;
    _sess = null;
    _setBusy(btnId, spinId, true, 'Checking...');
    if (errEl) errEl.textContent = '';
    const r = await _fetchKey(key);
    _setBusy(btnId, spinId, false);
    if (r.in_use) {
        _sess = null;
        if (errEl) errEl.textContent = 'Key in use by another session.';
        return;
    }
    if (r.ok && _sess) {
        if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; }
        _set('dds_key', key);
        _applyKey(r, key);
        _goPage('dash');
        _startPoll(key);
        _notify('Unlocked', `Welcome, ${r.label || 'user'}!`);
    } else {
        _sess = null;
        if (errEl) errEl.textContent = 'Invalid key: ' + (r.error || 'Unknown error');
    }
}

document.getElementById('ds-unlock-btn').addEventListener('click', () => {
    _unlock(document.getElementById('ds-key-in').value.trim(), 'ds-auth-err', 'ds-auth-spin', 'ds-unlock-btn', 'ds-unlock-lbl');
});
document.getElementById('ds-key-in').addEventListener('keydown', e => {
    if (e.key === 'Enter') _unlock(document.getElementById('ds-key-in').value.trim(), 'ds-auth-err', 'ds-auth-spin', 'ds-unlock-btn', 'ds-unlock-lbl');
});
document.getElementById('ds-free-btn').addEventListener('click', () => {
    _unlock(FREE_KEY, 'ds-auth-err', 'ds-auth-spin', 'ds-free-btn', null);
});
document.getElementById('ds-chkey-btn').addEventListener('click', () => {
    document.getElementById('ds-newkey-in').value = '';
    document.getElementById('ds-chkey-err').textContent = '';
    _goPage('chkey');
});
document.getElementById('ds-back-btn').addEventListener('click', () => { _goPage('dash'); });
document.getElementById('ds-newkey-btn').addEventListener('click', () => {
    _unlock(document.getElementById('ds-newkey-in').value.trim(), 'ds-chkey-err', 'ds-chkey-spin', 'ds-newkey-btn', null);
});
document.getElementById('ds-newkey-in').addEventListener('keydown', e => {
    if (e.key === 'Enter') _unlock(document.getElementById('ds-newkey-in').value.trim(), 'ds-chkey-err', 'ds-chkey-spin', 'ds-newkey-btn', null);
});
document.getElementById('ds-activefree-btn').addEventListener('click', async () => {
    _sess = null;
    const cdSaved = _loadCD();
    if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; }
    _setBusy('ds-activefree-btn', null, true, 'Activating...');
    const r = await _fetchKey(FREE_KEY);
    _setBusy('ds-activefree-btn', null, false);
    if (r.ok && _sess) {
        _set('dds_key', FREE_KEY);
        _applyKey(r, FREE_KEY);
        if (cdSaved > 0) _startCD(cdSaved, false);
        _goPage('dash');
        _startPoll(FREE_KEY);
        _notify('Free Tier Active', 'Now using the free public key.');
    } else {
        _sess = null;
        document.getElementById('ds-chkey-err').textContent = 'Could not activate free key. Try again.';
    }
});
document.getElementById('ds-blocked-chkey-btn').addEventListener('click', () => {
    _set('dds_key', '');
    _goPage('chkey');
});

document.getElementById('ds-create-btn').addEventListener('click', async () => {
    if (!_sess) return;
    const key  = _get('dds_key', '');
    if (!key) return;
    const btn  = document.getElementById('ds-create-btn');
    const prog = document.getElementById('ds-create-prog');
    const fill = document.getElementById('ds-create-fill');
    btn.disabled = true;
    prog.style.display = '';
    fill.style.width   = '20%';
    _logClear('ds-log-link');
    _log('ds-log-link', `Creating ${_keyData?.batch_size || '?'} links...`, 'i');
    fill.style.transition = 'width .8s';
    fill.style.width = '60%';
    const r = await _req('/api/link/create', 'POST', { key });
    fill.style.width = '100%';
    setTimeout(() => { prog.style.display = 'none'; fill.style.width = '0%'; fill.style.transition = ''; }, 500);
    btn.disabled = false;
    if (r.ok && r.urls && r.urls.length > 0) {
        _log('ds-log-link', `Created ${r.count} link${r.failed > 0 ? ` (${r.failed} failed)` : ''}`, 'o');
        r.urls.forEach((url, i) => { _log('ds-log-link', `#${i+1}  ${url}`, 'o'); _hist.unshift(url); });
        if (_keyData) { _keyData.used_today = (_keyData.used_today || 0) + r.count; _refreshQuota(_keyData.used_today, _keyData.daily_limit); }
        if (_keyData?.cd_seconds) _startCD(_keyData.cd_seconds);
    } else {
        const msg = r.error || 'Unknown error';
        _log('ds-log-link', `Error: ${msg}`, 'e');
        const cdM = msg.match(/Cooldown.*?(\d+):(\d+)/);
        if (cdM)                        _startCD(parseInt(cdM[1])*60+parseInt(cdM[2]));
        else if (r.cooldown_remain > 0) _startCD(r.cooldown_remain);
    }
});

function _getJwtUid() {
    let jwt = '';
    try { const m = document.cookie.match(new RegExp('(^| )jwt_token=([^;]+)')); if (m) jwt = decodeURIComponent(m[2]); } catch {}
    if (!jwt) return { jwt: null, uid: null };
    try {
        const seg = jwt.split('.')[1];
        const pad = seg + '='.repeat((4 - seg.length % 4) % 4);
        const uid = String(JSON.parse(atob(pad)).sub || '');
        return { jwt, uid };
    } catch { return { jwt: null, uid: null }; }
}

async function _activateFamilyDirect(jwt, uid, invite_code) {
    const url = `https://www.duolingo.com/2017-06-30/users/${uid}/family-plan/members/invite/${invite_code}`;
    return new Promise(resolve => {
        GM_xmlhttpRequest({
            method: 'POST', url,
            headers: {
                'Authorization': `Bearer ${jwt}`, 'Content-Type': 'application/json; charset=UTF-8',
                'Accept': 'application/json; charset=UTF-8', 'Origin': 'https://www.duolingo.com',
                'Referer': `https://www.duolingo.com/family-plan?invite=${invite_code}`,
                'User-Agent': navigator.userAgent, 'X-Requested-With': 'XMLHttpRequest',
                'X-Amzn-Trace-Id': `User=${uid}`
            },
            data: '{}', timeout: 15000,
            onload: r => {
                if (r.status === 200 || r.status === 201) {
                    try {
                        const d    = JSON.parse(r.responseText);
                        const secs = (d.secondaryMembers || []).map(String);
                        resolve({ ok: true, joined: secs.includes(String(uid)) });
                    } catch { resolve({ ok: true, joined: true }); }
                } else if (r.status === 409) {
                    resolve({ ok: true, already_member: true });
                } else if (r.status === 400) {
                    resolve({ ok: false, error: 'Account not eligible (previously used Super)' });
                } else {
                    resolve({ ok: false, error: `Duolingo returned ${r.status}` });
                }
            },
            onerror:   () => resolve({ ok: false, error: 'Network error' }),
            ontimeout: () => resolve({ ok: false, error: 'Timeout' })
        });
    });
}

document.getElementById('ds-activate-btn').addEventListener('click', async () => {
    if (!_sess) return;
    const { jwt, uid } = _getJwtUid();
    if (!jwt || !uid) return _log('ds-log-active', 'JWT not found. Please log into Duolingo first!', 'e');
    _setBusy('ds-activate-btn', null, true, 'Activating...');
    if (!_hist.length) {
        const key = _get('dds_key', '');
        if (!key) { _setBusy('ds-activate-btn', null, false); return _log('ds-log-active', 'No key found.', 'e'); }
        const lr = await _req(`/api/link/list?key=${encodeURIComponent(key)}`, 'GET');
        if (lr.ok && lr.links && lr.links.length) {
            _hist = lr.links;
        } else {
            _setBusy('ds-activate-btn', null, false);
            return _log('ds-log-active', 'No links available. Create links first.', 'e');
        }
    }
    const code = _hist[0].split('/').pop();
    _log('ds-log-active', `UID: ${uid} · Code: ${code}`, 'i');
    const r = await _activateFamilyDirect(jwt, uid, code);
    _setBusy('ds-activate-btn', null, false);
    if (r.ok) {
        _log('ds-log-active', r.already_member ? 'Already has Super.' : 'Activated! Check the app.', 'o');
        _notify('Super Activated', 'Free Super Duolingo activated!');
    } else {
        _log('ds-log-active', `Error: ${r.error || 'Failed'}`, 'e');
    }
});

const _COUPON_CODE = 'DUOBNBJUNE2026';

async function _applyCouponDirect(jwt, uid) {
    return new Promise(resolve => {
        GM_xmlhttpRequest({
            method: 'POST',
            url: `https://www.duolingo.com/2023-05-23/users/${uid}/shop-items`,
            headers: {
                'Accept': 'application/json; charset=UTF-8', 'Authorization': `Bearer ${jwt}`,
                'Content-Type': 'application/json', 'Origin': 'https://www.duolingo.com',
                'Referer': 'https://www.duolingo.com/settings/super', 'User-Agent': navigator.userAgent
            },
            data: JSON.stringify({ itemName: 'premium_subscription', couponCode: _COUPON_CODE, productId: 'com.duolingo.subscription.premium.prepaid', vendor: 'VENDOR_PREPAID' }),
            timeout: 15000,
            onload: r => {
                if (r.status === 409) return resolve({ ok: true, already_active: true });
                if (r.status === 200 || r.status === 201) return resolve({ ok: true });
                try { const d = JSON.parse(r.responseText); resolve({ ok: false, error: d.error || d.message || `Status ${r.status}` }); }
                catch { resolve({ ok: false, error: `Status ${r.status}` }); }
            },
            onerror:   () => resolve({ ok: false, error: 'Network error' }),
            ontimeout: () => resolve({ ok: false, error: 'Timeout' })
        });
    });
}

document.getElementById('ds-coupon-btn').addEventListener('click', async () => {
    if (!_sess) return;
    const { jwt, uid } = _getJwtUid();
    if (!jwt || !uid) return _log('ds-log-active', 'JWT not found. Please log into Duolingo first!', 'e');
    _setBusy('ds-coupon-btn', null, true, 'Processing...');
    _log('ds-log-active', `Applying 1-month coupon for UID: ${uid}`, 'i');
    const r = await _applyCouponDirect(jwt, uid);
    _setBusy('ds-coupon-btn', null, false);
    if (r.ok) {
        _log('ds-log-active', r.already_active ? 'Already has active Super.' : '1 month Super activated! Check the app.', 'o');
        _notify('1 Month Free!', 'Super Duolingo activated for 1 month!');
    } else {
        _log('ds-log-active', `Error: ${r.error || 'Failed'}`, 'e');
    }
});

document.getElementById('ds-loadhist-btn').addEventListener('click', async () => {
    if (!_sess) return;
    const key = _get('dds_key', '');
    if (!key) return;
    _setBusy('ds-loadhist-btn', null, true, 'Loading...');
    const r = await _req(`/api/link/list?key=${encodeURIComponent(key)}`, 'GET');
    _setBusy('ds-loadhist-btn', null, false);
    if (r.ok && r.links) {
        _hist = r.links;
        _renderHist();
        _notify('Loaded', `${r.links.length} links.`, 3);
    } else {
        _logErr('ds-hist-log', `Error: ${r.error || 'Unknown'}`);
    }
});

function _renderHist() {
    const el = document.getElementById('ds-hist-log');
    el.textContent = '';
    if (!_hist.length) {
        const s = document.createElement('span');
        s.className   = 'lw';
        s.textContent = 'No links yet.';
        el.appendChild(s);
        return;
    }
    _hist.forEach((url, i) => {
        const sUrl = document.createElement('span');
        sUrl.className   = 'lo';
        sUrl.textContent = `#${String(i+1).padStart(3,'0')} ${url}`;
        const sTag = document.createElement('span');
        sTag.className   = 'ds-copy-tag';
        sTag.textContent = 'copy';
        sTag.addEventListener('click', e => {
            e.stopPropagation();
            navigator.clipboard?.writeText(url).catch(() => {});
            sTag.textContent = '✓';
            setTimeout(() => { sTag.textContent = 'copy'; }, 1200);
        });
        el.appendChild(sUrl);
        el.appendChild(sTag);
        el.appendChild(document.createTextNode('\n'));
    });
}

document.getElementById('ds-copyall-btn').addEventListener('click', () => {
    if (!_hist.length) return;
    navigator.clipboard?.writeText(_hist.join('\n')).catch(() => {});
    const btn = document.getElementById('ds-copyall-btn');
    const orig = btn.textContent;
    btn.textContent = 'Copied!';
    setTimeout(() => { btn.textContent = orig; }, 1500);
});

(async function _boot() {
    let vr = null;
    const lastVerCheck = _get(VER_CHECK_STORE, 0);
    if (Date.now() - lastVerCheck > 86400000) {
        try { vr = await _checkVersion(); } catch {}
        if (vr !== null) _set(VER_CHECK_STORE, Date.now());
    }
    if (vr && vr.ok && vr.current_version && vr.current_version !== SCRIPT_VERSION) {
        const msg = document.getElementById('ds-update-msg');
        if (msg) msg.textContent = `v${SCRIPT_VERSION} is outdated. Latest: v${vr.current_version}. Please update to continue.`;
        const btn = document.getElementById('ds-update-btn');
        if (btn && vr.update_url) btn.href = vr.update_url;
        const fromEl = document.getElementById('ds-page-auth');
        if (fromEl) fromEl.classList.remove('active');
        const toEl = document.getElementById('ds-page-update');
        if (toEl) toEl.classList.add('active');
        _curPg = 'update';
        _toggleVis(false);
        return;
    }

    const saved = _get('dds_key', '');
    const key   = saved || FREE_KEY;
    const card  = document.getElementById('ds-card');
    const shell = document.getElementById('ds-shell');
    const h     = card.scrollHeight || 300;
    shell.style.bottom = `-${h - 6}px`;
    card.style.opacity = '0';

    _fetchKey(key).then(r => {
        if (r.in_use) {
            _set('dds_key', '');
            _goPage('blocked');
        } else if (r.ok && _sess) {
            if (r.expires_in_seconds != null && r.expires_in_seconds <= 0) {
                _set('dds_key', '');
                _fetchKey(FREE_KEY).then(fr => {
                    if (fr.ok && _sess) { _set('dds_key', FREE_KEY); _applyKey(fr, FREE_KEY); _goPage('dash'); _startPoll(FREE_KEY); }
                    else { _sess = null; _goPage('auth'); }
                });
                return;
            }
            if (!saved) _set('dds_key', FREE_KEY);
            _applyKey(r, key);
            _goPage('dash');
            _startPoll(key);
        } else {
            _sess = null;
            if (key !== FREE_KEY) _set('dds_key', '');
            document.getElementById('ds-key-in').value = saved || '';
        }
    });

    _toggleVis(false);
    setTimeout(() => {
        shell.style.transition = card.style.transition = '.55s cubic-bezier(.16,1,.32,1)';
        shell.style.bottom = '14px';
        card.style.opacity = '';
        setTimeout(() => { shell.style.transition = card.style.transition = ''; }, 560);
    }, 400);
})();

})();