DuoGang

Advanced Duolingo automation - Auto-solve lessons, farm XP, Gems, Streaks with beautiful UI

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         DuoGang
// @license mit
// @namespace    https://github.com/duochristian
// @version      1.0.0
// @description  Advanced Duolingo automation - Auto-solve lessons, farm XP, Gems, Streaks with beautiful UI
// @author      DuoGangTeam
// @match        *://*.duolingo.com/*
// @match        *://*.duolingo.cn/*
// @icon      https://raw.githubusercontent.com/JULIANGAMER587/Duogang_icon/main/Screenshot%202026-05-07%20151236-Photoroom.png
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      duolingo.com
// @connect      stories.duolingo.com
// @connect      goals-api.duolingo.com
// @connect      duolingo-leaderboards-prod.duolingo.com
// @connect      fonts.googleapis.com
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // ==================== STYLES ====================
    const fontLink = document.createElement('link');
    fontLink.rel = 'stylesheet';
    fontLink.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap';
    document.head.appendChild(fontLink);

    GM_addStyle(`
        :root {
            --DC-gold: 255, 179, 0;
            --DC-gold-dark: 204, 143, 0;
            --DC-blue: 28, 176, 246;
            --DC-green: 88, 204, 2;
            --DC-red: 255, 75, 75;
            --DC-purple: 206, 130, 255;
            /* Light mode defaults */
            --DC-bg: rgba(255,255,255,0.95);
            --DC-surface: rgba(248,248,248,0.9);
            --DC-text: #1a1a1a;
            --DC-text-sub: #888;
            --DC-divider: rgba(0,0,0,0.08);
            --DC-outline: rgba(0,0,0,0.08);
            --DC-input-bg: rgba(255,179,0,0.05);
            --DC-input-outline: rgba(255,179,0,0.2);
            --DC-notif-bg: rgba(255,255,255,0.97);
            --DC-shadow: rgba(0,0,0,0.10);
        }
        #DC_Root.DC_Dark {
            --DC-bg: rgba(22,22,28,0.97);
            --DC-surface: rgba(30,30,38,0.95);
            --DC-text: #f0f0f0;
            --DC-text-sub: #aaa;
            --DC-divider: rgba(255,255,255,0.08);
            --DC-outline: rgba(255,255,255,0.08);
            --DC-input-bg: rgba(255,179,0,0.07);
            --DC-input-outline: rgba(255,179,0,0.25);
            --DC-notif-bg: rgba(28,28,36,0.98);
            --DC-shadow: rgba(0,0,0,0.40);
        }

        #DC_Root * { box-sizing: border-box; }
        #DC_Root p, #DC_Root span, #DC_Root button, #DC_Root input, #DC_Root label, #DC_Root div {
            font-family: 'Inter', 'din-round', -apple-system, sans-serif !important;
        }
        #DC_Root p, #DC_Root span { margin: 0; padding: 0; }

        .DC_Main {
            display: inline-flex; flex-direction: column;
            justify-content: flex-end; align-items: flex-end;
            gap: 10px; position: fixed; right: 16px; bottom: 16px;
            z-index: 2147483647; transition: bottom 0.6s cubic-bezier(0.16,1,0.32,1);
            user-select: none;
        }
        .DC_Main.DC_Dragging { transition: none !important; }
        @media (max-width: 699px) { .DC_Main { margin-bottom: 80px; } }

        .DC_Main_Box {
            display: flex; width: 320px; padding: 18px;
            flex-direction: column; gap: 12px;
            border-radius: 20px;
            outline: 2px solid var(--DC-outline); outline-offset: -2px;
            background: var(--DC-bg);
            backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
            box-shadow: 0 8px 32px var(--DC-shadow);
            transition: opacity 0.6s cubic-bezier(0.16,1,0.32,1), filter 0.6s cubic-bezier(0.16,1,0.32,1);
        }

        .DC_HStack { display:flex; align-items:center; gap:8px; align-self:stretch; }
        .DC_HStack_Between { display:flex; align-items:center; justify-content:space-between; align-self:stretch; }
        .DC_VStack { display:flex; flex-direction:column; gap:8px; align-self:stretch; }
        .DC_Divider { height:1px; background:var(--DC-divider); align-self:stretch; flex-shrink:0; }

        .DC_Title { font-size:15px; font-weight:700; color:var(--DC-text); }
        .DC_Subtitle { font-size:12px; font-weight:500; color:var(--DC-text-sub); }

        .DC_Btn {
            display:flex; height:44px; padding:10px 16px;
            align-items:center; gap:8px;
            border-radius:12px; border:none; cursor:pointer;
            font-size:14px; font-weight:700;
            transition: all 0.3s cubic-bezier(0.16,1,0.32,1);
            user-select:none; -webkit-user-select:none;
        }
        .DC_Btn:hover { filter:brightness(0.9); transform:scale(1.03); }
        .DC_Btn:active { filter:brightness(0.85); transform:scale(0.97); }

        .DC_Btn_Primary { background:rgb(var(--DC-gold)); color:#1a1a1a; }
        .DC_Btn_Blue { background:rgb(var(--DC-blue)); color:#fff; }
        .DC_Btn_Green { background:rgb(var(--DC-green)); color:#fff; }
        .DC_Btn_Red { background:rgb(var(--DC-red)); color:#fff; }
        .DC_Btn_Ghost {
            background:rgba(var(--DC-gold),0.1); color:rgb(var(--DC-gold-dark));
            outline:2px solid rgba(var(--DC-gold),0.2); outline-offset:-2px;
        }
        .DC_Btn_Icon { flex:none; width:44px; padding:10px; justify-content:center; }

        .DC_Input_Wrap {
            display:flex; height:48px; padding:0 16px;
            align-items:center; gap:8px; flex:1;
            border-radius:12px;
            outline:2px solid var(--DC-input-outline); outline-offset:-2px;
            background:var(--DC-input-bg);
        }
        .DC_Input {
            border:none; outline:none; background:none;
            font-size:16px; font-weight:600; color:rgb(var(--DC-gold-dark));
            text-align:right; width:100%; font-family:inherit;
            -moz-appearance:textfield;
        }
        .DC_Input::placeholder { color:rgba(var(--DC-gold-dark),0.4); }
        .DC_Input::-webkit-outer-spin-button,
        .DC_Input::-webkit-inner-spin-button { -webkit-appearance:none; margin:0; }

        .DC_Input_Btn {
            display:flex; height:48px; padding:0 18px;
            align-items:center; gap:6px;
            border-radius:12px; border:none; cursor:pointer;
            font-size:14px; font-weight:800; color:#1a1a1a;
            background:rgb(var(--DC-gold));
            transition: all 0.3s cubic-bezier(0.16,1,0.32,1);
            white-space:nowrap; flex-shrink:0;
        }
        .DC_Input_Btn:hover { filter:brightness(0.9); transform:scale(1.03); }
        .DC_Input_Btn:active { filter:brightness(0.85); transform:scale(0.97); }
        .DC_Input_Btn:disabled { opacity:0.4; pointer-events:none; }

        .DC_Prog_Wrap { height:0; border-radius:4px; background:rgba(var(--DC-gold),0.1); overflow:hidden; transition:height 0.4s; }
        .DC_Prog_Wrap.on { height:4px; }
        .DC_Prog_Fill { height:100%; border-radius:4px; background:linear-gradient(90deg, rgb(var(--DC-gold)), rgb(var(--DC-gold-dark))); width:0%; transition:width 0.5s; }

        .DC_Page { display:none; flex-direction:column; gap:12px; }
        .DC_Page.active { display:flex; }

        .DC_Notif {
            position:fixed; left:50%; transform:translateX(-50%); bottom:16px;
            z-index:2147483647; padding:14px 18px;
            border-radius:14px; background:var(--DC-notif-bg);
            backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px);
            box-shadow:0 8px 32px var(--DC-shadow);
            font-size:13px; font-weight:600; color:var(--DC-text);
            transition:all 0.4s cubic-bezier(0.16,1,0.32,1);
            opacity:0; pointer-events:none;
        }
        .DC_Notif.show { opacity:1; pointer-events:auto; }

        .DC_User_Row { display:none; gap:10px; }
        .DC_Avatar { width:36px; height:36px; border-radius:50%; background:rgba(var(--DC-gold),0.1); overflow:hidden; flex-shrink:0; display:flex; align-items:center; justify-content:center; font-size:18px; }
        .DC_Stat { display:flex; align-items:center; gap:4px; }
        .DC_Stat img { width:16px; height:16px; }
        .DC_Stat span { font-size:12px; font-weight:600; color:var(--DC-text-sub); }

        /* Drag handle cursor */
        .DC_Drag_Handle { cursor: grab; }
        .DC_Drag_Handle:active { cursor: grabbing; }

        /* YouTube link button */
        .DC_YT_Btn {
            display:flex; align-items:center; gap:6px; height:44px; padding:0 12px;
            border-radius:12px; border:none; cursor:pointer; text-decoration:none;
            background:rgba(255,0,0,0.1); color:#cc0000;
            outline:2px solid rgba(255,0,0,0.18); outline-offset:-2px;
            font-size:12px; font-weight:700; transition:all 0.3s cubic-bezier(0.16,1,0.32,1);
            white-space:nowrap; flex-shrink:0;
        }
        #DC_Root.DC_Dark .DC_YT_Btn { background:rgba(255,0,0,0.15); color:#ff5555; outline-color:rgba(255,80,80,0.25); }
        .DC_YT_Btn:hover { background:rgba(255,0,0,0.18); transform:scale(1.04); }
        .DC_YT_Btn:active { transform:scale(0.97); }

        /* Dark mode toggle */
        .DC_DarkToggle {
            display:flex; align-items:center; justify-content:center;
            width:44px; height:44px; border-radius:12px; border:none; cursor:pointer;
            background:rgba(var(--DC-gold),0.10); outline:2px solid rgba(var(--DC-gold),0.2); outline-offset:-2px;
            transition:all 0.3s cubic-bezier(0.16,1,0.32,1); flex-shrink:0;
        }
        .DC_DarkToggle:hover { filter:brightness(0.88); transform:scale(1.05); }
        .DC_DarkToggle:active { transform:scale(0.95); }

        /* Eye icon states */
        .DC_Eye_Open { display:block; }
        .DC_Eye_Closed { display:none; }
        .DC_Hide_Btn_Hidden .DC_Eye_Open { display:none; }
        .DC_Hide_Btn_Hidden .DC_Eye_Closed { display:block; }

        .DC_Solver_Btn {
            position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%);
            z-index: 2147483647; display: flex; gap: 10px;
            animation: DC_SlideUp 0.3s ease-out;
        }
        @keyframes DC_SlideUp {
            from { opacity:0; transform:translateX(-50%) translateY(20px); }
            to { opacity:1; transform:translateX(-50%) translateY(0); }
        }
        .DC_Solver_Btn button {
            padding: 12px 24px; border:none; border-radius: 12px;
            font-size: 14px; font-weight: 700; cursor:pointer;
            transition: all 0.2s; color:#fff;
        }
        .DC_Solver_Btn button:hover { filter:brightness(1.1); transform:translateY(-2px); }
        .DC_Solver_Btn button:active { transform:translateY(2px); }
        .DC_Solve_One { background:rgb(var(--DC-green)); border-bottom:4px solid #46a302; }
        .DC_Solve_All { background:rgb(var(--DC-gold)); border-bottom:4px solid rgb(var(--DC-gold-dark)); color:#1a1a1a; }

        .DC_Toggle { position:relative; width:44px; height:26px; flex-shrink:0; cursor:pointer; }
        .DC_Toggle input { opacity:0; width:0; height:0; }
        .DC_Toggle_Slider { position:absolute; inset:0; background:rgba(0,0,0,0.15); border-radius:26px; transition:0.3s; }
        .DC_Toggle_Slider:before { content:''; position:absolute; width:20px; height:20px; left:3px; bottom:3px; background:#fff; border-radius:50%; transition:0.3s; box-shadow:0 2px 4px rgba(0,0,0,0.2); }
        .DC_Toggle input:checked + .DC_Toggle_Slider { background:rgb(var(--DC-gold)); }
        .DC_Toggle input:checked + .DC_Toggle_Slider:before { transform:translateX(18px); }
    `);

    // ==================== HTML STRUCTURE ====================
    const wrap = document.createElement('div');
    wrap.id = 'DC_Root';
    wrap.innerHTML = `
        <div class="DC_Notif" id="DC_Notif"></div>
        <div class="DC_Main" id="DC_Main">
            <div class="DC_HStack" style="align-self:flex-end; gap:8px;">
                <!-- YouTube link -->
                <a class="DC_YT_Btn" href="https://www.youtube.com/@JULSEXE_Edits/shorts?app=desktop" target="_blank" rel="noopener noreferrer" title="Follow us on YouTube">
                    <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
                        <path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
                    </svg>
                    Follow us on YouTube
                </a>
                <!-- Dark mode toggle -->
                <button class="DC_DarkToggle" id="DC_Dark_Btn" title="Toggle dark/light mode">
                    <svg id="DC_Dark_Icon_Moon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="rgb(var(--DC-gold-dark))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
                    </svg>
                    <svg id="DC_Dark_Icon_Sun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="rgb(var(--DC-gold))" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;">
                        <circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
                    </svg>
                </button>
                <!-- Hide/Show button with eye icon -->
                <div class="DC_Btn DC_Drag_Handle" id="DC_Hide_Btn" style="background:rgb(var(--DC-gold)); height:44px; padding:0 14px; gap:8px; border-radius:12px; cursor:pointer; display:flex; align-items:center;" title="Drag to move · Click to show/hide">
                    <!-- Eye Open (menu visible) -->
                    <svg class="DC_Eye_Open" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#1a1a1a" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
                        <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>
                    <!-- Eye Closed (menu hidden) -->
                    <svg class="DC_Eye_Closed" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#1a1a1a" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
                        <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 style="color:#1a1a1a;font-size:13px;font-weight:700;" id="DC_Hide_Label">Hide</span>
                </div>
            </div>
            <div class="DC_Main_Box" id="DC_Main_Box">
                <div class="DC_Page active" id="DC_Page_Main">
                    <div class="DC_HStack">
                        <div class="DC_Btn DC_Btn_Ghost" id="DC_Conn_Btn" style="flex:1;">
                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" id="DC_Conn_Ico">
                                <circle cx="12" cy="12" r="10" stroke="rgb(var(--DC-gold-dark))" stroke-width="2" fill="none"/>
                            </svg>
                            <span class="DC_Title" id="DC_Conn_Txt" style="color:rgb(var(--DC-gold-dark));">Connecting</span>
                        </div>
                    </div>

                    <div class="DC_User_Row DC_HStack" id="DC_User_Row">
                        <div class="DC_Avatar" id="DC_Avatar">?</div>
                        <div style="flex:1;min-width:0;">
                            <p class="DC_Title" id="DC_UName" style="font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></p>
                            <div class="DC_HStack" style="gap:12px;">
                                <div class="DC_Stat"><img src="https://d35aaqx5ub95lt.cloudfront.net/images/profile/01ce3a817dd01842581c3d18debcbc46.svg"><span id="DC_UXP">0</span></div>
                                <div class="DC_Stat"><img src="https://d35aaqx5ub95lt.cloudfront.net/images/gems/45c14e05be9c1af1d7d0b54c6eed7eee.svg"><span id="DC_UGems">0</span></div>
                                <div class="DC_Stat"><img src="https://d35aaqx5ub95lt.cloudfront.net/images/icons/398e4298a3b39ce566050e5c041949ef.svg"><span id="DC_UStreak">0</span></div>
                            </div>
                        </div>
                    </div>

                    <div class="DC_Divider"></div>

                    <div class="DC_VStack">
                        <p class="DC_Title">XP to farm</p>
                        <div class="DC_HStack">
                            <div class="DC_Input_Wrap">
                                <input type="number" class="DC_Input" id="DC_XP_Input" placeholder="0" min="30" max="500000">
                            </div>
                            <button class="DC_Input_Btn" id="DC_XP_Btn" disabled>
                                <span>GET XP</span>
                                <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.56 5.82 22 7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
                            </button>
                        </div>
                        <div class="DC_Prog_Wrap" id="DC_XP_Prog"><div class="DC_Prog_Fill" id="DC_XP_Fill"></div></div>
                    </div>

                    <div class="DC_VStack">
                        <p class="DC_Title">Farm Gems</p>
                        <div class="DC_HStack">
                            <div class="DC_Input_Wrap">
                                <input type="number" class="DC_Input" id="DC_Gem_Input" placeholder="0" readonly>
                            </div>
                            <button class="DC_Input_Btn" id="DC_Gem_Btn" disabled>
                                <span>RUN</span>
                                <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
                            </button>
                        </div>
                    </div>

                    <div class="DC_VStack">
                        <p class="DC_Title">Streak days to add</p>
                        <div class="DC_HStack">
                            <div class="DC_Input_Wrap">
                                <input type="number" class="DC_Input" id="DC_Streak_Input" placeholder="0" min="1" max="3650">
                            </div>
                            <button class="DC_Input_Btn" id="DC_Streak_Btn" disabled>
                                <span>RUN</span>
                                <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M13.5.67s.74 2.65.74 4.8c0 2.06-1.35 3.73-3.41 3.73-2.07 0-3.63-1.67-3.63-3.73l.03-.36C5.21 7.51 4 10.62 4 14c0 4.42 3.58 8 8 8s8-3.58 8-8C20 8.61 17.41 3.8 13.5.67zM11.71 19c-1.78 0-3.22-1.4-3.22-3.14 0-1.62 1.05-2.76 2.81-3.12 1.77-.36 3.6-1.21 4.62-2.58.39 1.29.59 2.65.59 4.04 0 2.65-2.15 4.8-4.8 4.8z"/></svg>
                            </button>
                        </div>
                        <div class="DC_Prog_Wrap" id="DC_Streak_Prog"><div class="DC_Prog_Fill" id="DC_Streak_Fill"></div></div>
                    </div>

                    <div class="DC_Divider"></div>

                    <div class="DC_HStack_Between">
                        <button class="DC_Btn DC_Btn_Ghost" id="DC_Practice_Btn" style="flex:1;justify-content:center;" disabled>
                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" stroke="rgb(var(--DC-gold-dark))" stroke-width="2" stroke-linecap="round"/></svg>
                            Practice Farm
                        </button>
                        <button class="DC_Btn DC_Btn_Ghost" id="DC_Super_Btn" style="flex:1;justify-content:center;" disabled>
                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.56 5.82 22 7 14.14 2 9.27l6.91-1.01L12 2z" fill="rgb(var(--DC-gold-dark))"/></svg>
                            Free Super
                        </button>
                    </div>

                    <div class="DC_HStack_Between">
                        <div class="DC_HStack" style="gap:4px;">
                            <span class="DC_Subtitle">Auto Solver</span>
                            <label class="DC_Toggle">
                                <input type="checkbox" id="DC_Solver_Toggle">
                                <span class="DC_Toggle_Slider"></span>
                            </label>
                        </div>
                        <span class="DC_Subtitle">DuoGang v2.0.2</span>
                    </div>
                </div>
            </div>
        </div>
    `;
    document.body.appendChild(wrap);

    // ==================== STATE ====================
    let jwt = null, sub = null, headers = null, user = null;
    let running = false, task = null, hidden = false;
    let delay = parseInt(localStorage.getItem('dc_delay') || '500', 10);
    let earnedGems = 0;

    const sleep = ms => new Promise(r => setTimeout(r, ms));

    // ==================== HELPERS ====================
    function getJwt() {
        const m = document.cookie.match(/(^| )jwt_token=([^;]+)/);
        return m ? m[2] : null;
    }

    function decodeJwt(t) {
        try {
            const b = t.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
            const pad = b.padEnd(b.length + (4 - b.length % 4) % 4, '=');
            return JSON.parse(decodeURIComponent(atob(pad).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')));
        } catch { return null; }
    }

    function buildHeaders(j) {
        return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + j, 'User-Agent': navigator.userAgent };
    }

    function gm(method, url, data, hdrs) {
        return new Promise((res, rej) => GM_xmlhttpRequest({
            method, url, headers: hdrs || headers,
            data: data ? JSON.stringify(data) : null,
            onload: r => res(r), onerror: () => rej(new Error('Network')),
            timeout: 15000, ontimeout: () => rej(new Error('Timeout'))
        }));
    }

    function notif(msg, dur = 4) {
        const el = document.getElementById('DC_Notif');
        el.textContent = msg;
        el.classList.add('show');
        clearTimeout(window._dcNotifTimer);
        window._dcNotifTimer = setTimeout(() => el.classList.remove('show'), dur * 1000);
    }

    function setConn(state, label) {
        const btn = document.getElementById('DC_Conn_Btn');
        const txt = document.getElementById('DC_Conn_Txt');
        const ico = document.getElementById('DC_Conn_Ico');
        txt.textContent = label || state;
        ico.innerHTML = state === 'Connected' ?
            '<circle cx="12" cy="12" r="10" fill="rgb(var(--DC-green))"/><path d="M7 12l3 3 7-7" stroke="#fff" stroke-width="2" stroke-linecap="round"/>' :
            '<circle cx="12" cy="12" r="10" stroke="rgb(var(--DC-gold-dark))" stroke-width="2" fill="none"/>';
        if (state === 'Connected') {
            document.getElementById('DC_User_Row').style.display = 'flex';
        }
    }

    function setBtnState(btnId, cfg, label) {
        const btn = document.getElementById(btnId);
        if (!btn) return;
        btn.disabled = false;
        btn.className = cfg.cls || 'DC_Input_Btn';
        const span = btn.querySelector('span');
        if (span) span.textContent = label;
    }

    function resetBtn(btnId, label) {
        const btn = document.getElementById(btnId);
        if (!btn) return;
        btn.disabled = !user;
        setBtnState(btnId, { cls: 'DC_Input_Btn' }, label);
        const prog = document.getElementById(btnId.replace('_Btn', '_Prog'));
        if (prog) setTimeout(() => prog.classList.remove('on'), 2000);
    }

    function setBtnProgress(btnId, pct) {
        const fill = document.getElementById(btnId.replace('_Btn', '_Fill'));
        if (fill) fill.style.width = pct + '%';
        const btn = document.getElementById(btnId);
        if (btn) {
            const span = btn.querySelector('span');
            if (span) span.textContent = pct + '%';
        }
    }

    function setBtnRunning(btnId) {
        const btn = document.getElementById(btnId);
        if (!btn) return;
        btn.disabled = false;
        setBtnState(btnId, { cls: 'DC_Input_Btn DC_Btn_Red' }, 'STOP');
        const prog = document.getElementById(btnId.replace('_Btn', '_Prog'));
        if (prog) prog.classList.add('on');
    }

    function setBtnDone(btnId, label) {
        setBtnState(btnId, { cls: 'DC_Input_Btn DC_Btn_Green' }, label || 'DONE');
        const fill = document.getElementById(btnId.replace('_Btn', '_Fill'));
        if (fill) fill.style.width = '100%';
    }

    // ==================== CONNECTION ====================
    async function connect() {
        setConn('Connecting');
        jwt = getJwt();
        if (!jwt) { setConn('Not logged in'); return; }
        const dec = decodeJwt(jwt);
        if (!dec) { setConn('Invalid token'); return; }
        sub = dec.sub;
        headers = buildHeaders(jwt);
        try {
            const r = await gm('GET', `https://www.duolingo.com/2017-06-30/users/${sub}?fields=id,username,fromLanguage,learningLanguage,streak,totalXp,gems,picture,streakData`);
            if (r.status !== 200) throw new Error(r.status);
            user = JSON.parse(r.responseText);
            setConn('Connected');
            renderUser(user);
            ['DC_XP_Btn', 'DC_Gem_Btn', 'DC_Streak_Btn', 'DC_Practice_Btn', 'DC_Super_Btn'].forEach(id => {
                const b = document.getElementById(id);
                if (b) b.disabled = false;
            });
        } catch (e) {
            setConn('Error - retrying');
            setTimeout(connect, 8000);
        }
    }

    function renderUser(u) {
        if (!u) return;
        document.getElementById('DC_UName').textContent = u.username || '';
        document.getElementById('DC_UXP').textContent = (u.totalXp || 0).toLocaleString();
        document.getElementById('DC_UGems').textContent = (u.gems || 0).toLocaleString();
        document.getElementById('DC_UStreak').textContent = (u.streak || 0).toLocaleString();
        if (u.picture) {
            let hq = u.picture.replace(/\/(medium|large|small)$/, '/xlarge');
            if (!hq.endsWith('/xlarge') && hq.includes('duolingo.com/ssr-avatars')) hq += '/xlarge';
            const av = document.getElementById('DC_Avatar');
            const img = document.createElement('img');
            img.src = hq;
            img.style.cssText = 'width:100%;height:100%;object-fit:cover;border-radius:50%;';
            img.onerror = function() { av.innerHTML = '?'; };
            av.innerHTML = '';
            av.appendChild(img);
        }
    }

    // ==================== XP FARM ====================
    async function farmXP(txp) {
        const MAX = 499;
        let loops = Math.floor(txp / MAX), rem = txp % MAX;
        if (rem > 0 && rem < 30 && loops > 0) { loops--; rem += MAX; }
        const total = loops + (rem >= 30 ? 1 : 0);
        let cur = 0, earned = 0;
        setBtnRunning('DC_XP_Btn');
        for (let i = 0; i < loops; i++) {
            if (!running) break;
            const ok = await storyXP(469);
            if (ok) { earned += MAX; cur++; }
            setBtnProgress('DC_XP_Btn', Math.floor((cur / total) * 100));
            await sleep(delay);
        }
        if (rem >= 30 && running) {
            const ok = await storyXP(Math.min(rem - 30, 469));
            if (ok) { earned += rem; cur++; }
            setBtnProgress('DC_XP_Btn', 100);
        }
        if (running) {
            setBtnDone('DC_XP_Btn', 'DONE');
            notif('Farmed ' + earned.toLocaleString() + ' XP!');
            setTimeout(connect, 1500);
            setTimeout(() => resetBtn('DC_XP_Btn', 'GET XP'), 3000);
        }
    }

    async function storyXP(hh) {
        try {
            const now = Math.floor(Date.now() / 1000), dur = Math.floor(Math.random() * 121 + 300);
            const r = await gm('POST', 'https://stories.duolingo.com/api2/stories/fr-en-le-passeport/complete', {
                awardXp: true, completedBonusChallenge: true,
                fromLanguage: 'fr', learningLanguage: 'en',
                hasXpBoost: false, illustrationFormat: 'svg',
                isFeaturedStoryInPracticeHub: true, isLegendaryMode: true,
                isV2Redo: false, isV2Story: false, masterVersion: true,
                maxScore: 0, score: 0, happyHourBonusXp: hh,
                startTime: now, endTime: now + dur
            });
            return r.status === 200;
        } catch { return false; }
    }

    // ==================== GEM FARM ====================
    async function getGemRewards() {
        try {
            const r = await gm('GET', `https://www.duolingo.com/2023-05-23/users/${sub}?fields=rewardBundles`);
            if (r.status !== 200) return [];
            const bundles = JSON.parse(r.responseText).rewardBundles || [];
            const gems = [];
            for (const bundle of bundles) {
                for (const reward of bundle.rewards || []) {
                    if (!reward.consumed && (reward.id.includes('GEMS') || reward.currency === 'GEMS')) {
                        gems.push({ id: reward.id, amount: reward.amount || 0 });
                    }
                }
            }
            return gems;
        } catch { return []; }
    }

    async function exploitGemReward(rewardId) {
        const body = {
            consumed: true,
            fromLanguage: user.fromLanguage,
            learningLanguage: user.learningLanguage,
            pathLevelSpecifics: {
                anchorSkillId: 'f22fd38157eea63965dc39eeac3c40c1',
                indexSinceAnchorSkill: 0,
                treeId: '14b1a2672c1bb3b250ebaa31b86c343e',
                nodeState: 'active'
            }
        };
        try {
            const r = await gm('PATCH', `https://www.duolingo.com/2023-05-23/users/${sub}/rewards/${rewardId}`, body);
            return r.status === 200;
        } catch { return false; }
    }

    async function getGemCount() {
        try {
            const r = await gm('GET', `https://www.duolingo.com/2023-05-23/users/${sub}?fields=gemsConfig`);
            if (r.status !== 200) return null;
            return JSON.parse(r.responseText).gemsConfig?.gems ?? null;
        } catch { return null; }
    }

    async function farmGems() {
        earnedGems = 0;
        const gemInput = document.getElementById('DC_Gem_Input');
        if (gemInput) gemInput.value = '';
        setBtnRunning('DC_Gem_Btn');

        outer: while (running && task === 'gems') {
            const rewards = await getGemRewards();
            if (rewards.length === 0) { await sleep(delay * 2); continue; }

            let gemsBefore = await getGemCount() ?? (user?.gems ?? 0);

            const THREADS = 3;
            for (let i = 0; i < rewards.length; i += THREADS) {
                if (!running || task !== 'gems') break outer;
                const batch = rewards.slice(i, i + THREADS);
                await Promise.all(batch.map(r => exploitGemReward(r.id)));
                await sleep(200);

                const now = await getGemCount();
                if (now !== null) {
                    const gained = Math.max(0, now - gemsBefore);
                    earnedGems += gained;
                    gemsBefore = now;
                    if (gemInput) gemInput.value = String(earnedGems);
                    if (user) { user.gems = now; renderUser(user); }
                }
                await sleep(Math.max(100, delay));
            }
            await sleep(delay);
        }

        resetBtn('DC_Gem_Btn', 'RUN');
        const btn = document.getElementById('DC_Gem_Btn');
        if (btn) btn.disabled = !user;
        if (earnedGems > 0) {
            notif('+' + earnedGems.toLocaleString() + ' gems gained!');
            setTimeout(connect, 1500);
        }
    }

    // ==================== STREAK FARM ====================
    async function farmStreak(days) {
        const CH = ["assist", "characterIntro", "characterMatch", "characterPuzzle", "characterSelect", "characterTrace", "characterWrite", "completeReverseTranslation", "definition", "dialogue", "extendedMatch", "extendedListenMatch", "form", "freeResponse", "gapFill", "judge", "listen", "listenComplete", "listenMatch", "match", "name", "listenComprehension", "listenIsolation", "listenSpeak", "listenTap", "orderTapComplete", "partialListen", "partialReverseTranslate", "patternTapComplete", "radioBinary", "radioImageSelect", "radioListenMatch", "radioListenRecognize", "radioSelect", "readComprehension", "reverseAssist", "sameDifferent", "select", "selectPronunciation", "selectTranscription", "svgPuzzle", "syllableTap", "syllableListenTap", "speak", "tapCloze", "tapClozeTable", "tapComplete", "tapCompleteTable", "tapDescribe", "translate", "transliterate", "transliterationAssist", "typeCloze", "typeClozeTable", "typeComplete", "typeCompleteTable", "writeComprehension"];
        let farmStart;
        try {
            const s = new Date(user.streakData?.currentStreak?.startDate || Date.now());
            s.setDate(s.getDate() - 1);
            farmStart = s;
        } catch { const n = new Date(); n.setDate(n.getDate() - 1); farmStart = n; }
        setBtnRunning('DC_Streak_Btn');
        for (let i = 0; i < days; i++) {
            if (!running) break;
            const simDay = new Date(farmStart);
            simDay.setDate(simDay.getDate() - i);
            const end = Math.floor(simDay.getTime() / 1000);
            try {
                const sr = await gm('POST', 'https://www.duolingo.com/2023-05-23/sessions', {
                    challengeTypes: CH, fromLanguage: user.fromLanguage, isFinalLevel: false, isV2: true,
                    juicy: true, learningLanguage: user.learningLanguage, smartTipsVersion: 2, type: 'GLOBAL_PRACTICE'
                });
                if (sr.status === 200) {
                    const sess = JSON.parse(sr.responseText);
                    await new Promise((res, rej) => GM_xmlhttpRequest({
                        method: 'PUT', url: `https://www.duolingo.com/2023-05-23/sessions/${sess.id}`,
                        headers: headers, data: JSON.stringify({
                            ...sess, heartsLeft: 5, startTime: end - 1, endTime: end,
                            enableBonusPoints: false, failed: false, maxInLessonStreak: 9, shouldLearnThings: true
                        }),
                        onload: r => res(r), onerror: () => rej(), timeout: 15000, ontimeout: () => rej()
                    }));
                }
            } catch { }
            setBtnProgress('DC_Streak_Btn', Math.floor(((i + 1) / days) * 100));
            await sleep(delay);
        }
        if (running) {
            setBtnDone('DC_Streak_Btn', 'DONE');
            notif('Restored ' + days + ' streak days!');
            setTimeout(connect, 1500);
            setTimeout(() => resetBtn('DC_Streak_Btn', 'RUN'), 3000);
        }
    }

    // ==================== RUN LOGIC ====================
    async function run(type, val) {
        if (running) { running = false; notif('Farm stopped.'); return; }
        if (!user) { notif('Please wait for connection.'); return; }
        running = true; task = type;
        try {
            if (type === 'xp') await farmXP(val);
            if (type === 'gems') await farmGems();
            if (type === 'streak') await farmStreak(val);
        } catch (e) { notif('Error: ' + e.message); }
        if (!running) {
            if (type === 'xp') resetBtn('DC_XP_Btn', 'GET XP');
            if (type === 'streak') resetBtn('DC_Streak_Btn', 'RUN');
        }
        running = false; task = null;
    }

    // ==================== AUTO SOLVER ====================
    let solverUI = null;
    let isAutoMode = false;
    let solverEnabled = localStorage.getItem('dc_solver') === 'true';

    const solver = {
        findReact: (dom, traverseUp = 1) => {
            const key = Object.keys(dom).find(k => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$"));
            if (!key) return null;
            const domFiber = dom[key];
            if (!domFiber) return null;
            const GetCompFiber = fiber => {
                let parentFiber = fiber.return;
                while (typeof parentFiber.type == "string") parentFiber = parentFiber.return;
                return parentFiber;
            };
            let compFiber = GetCompFiber(domFiber);
            for (let i = 0; i < traverseUp; i++) compFiber = GetCompFiber(compFiber);
            return compFiber.stateNode;
        },

        determineType: () => {
            try {
                const t = window.sol?.type;
                if (!t) return false;
                if (t === 'speak' || t === 'listenSpeak' || document.querySelector('[data-test*="challenge-speak"]')) return 'Challenge Speak';
                if (t === 'listenMatch') return 'Listen Match';
                if (document.querySelector('.FmlUF')) {
                    if (t === 'arrange') return 'Story Arrange';
                    if (t === 'multiple-choice' || t === 'select-phrases') return 'Story Multiple Choice';
                    if (t === 'point-to-phrase') return 'Story Point to Phrase';
                    if (t === 'match') return 'Story Pairs';
                }
                if (t === 'typeCloze') return 'Type Cloze';
                if (t === 'typeClozeTable') return 'Type Cloze Table';
                if (t === 'tapClozeTable') return 'Tap Cloze Table';
                if (t === 'typeCompleteTable') return 'Type Complete Table';
                if (t === 'tapCompleteTable') return 'Tap Complete Table';
                if (t === 'patternTapComplete') return 'Pattern Tap Complete';
                if (t === 'syllableTap') return 'Syllable Tap';
                if (t === 'syllableListenTap') return 'Syllable Listen Tap';
                if (t === 'listenTap') return 'Listen Tap';
                if (t === 'listen') return 'Listen Type';
                if (t === 'translate') return 'Translate';
                if (t === 'completeReverseTranslation') return 'Complete Reverse';
                if (document.querySelector('[data-test*="challenge-partialReverseTranslate"]')) return 'Partial Reverse';
                if (document.querySelector('[data-test="challenge challenge-characterWrite"]')) {
                    if (document.querySelector('g._25Ktp')) return 'Character Write Drag';
                    if (document.querySelector('path._1e5Zt')) return 'Character Write Draw';
                    return 'Character Write Freehand';
                }
                if (t === 'judge') return 'Judge';
                if (t === 'dialogue' || t === 'characterIntro' || t === 'selectTranscription') return 'Dialogue';
                if (t === 'characterMatch' || t === 'match') {
                    if (document.querySelector('[data-test$="challenge-tap-token"]')) return 'Pairs';
                }
                if (t === 'select' || t === 'characterSelect' || t === 'form' || t === 'readComprehension' || t === 'listenComprehension' || t === 'selectPronunciation') return 'Select Card';
                if (document.querySelector('[data-test*="challenge-name"]') && document.querySelector('[data-test="challenge-choice"]')) return 'Challenge Name';
                if (document.querySelector('[data-test="challenge-choice"]')) {
                    if (document.querySelector('[data-test="challenge-text-input"]')) return 'Challenge Choice with Text Input';
                    return 'Challenge Choice';
                }
                if (document.querySelector('[data-test$="challenge-tap-token"]')) {
                    if (window.sol?.pairs !== undefined) return 'Pairs';
                    if (window.sol?.correctTokens !== undefined) return 'Tokens Run';
                    if (window.sol?.correctIndices !== undefined) return 'Indices Run';
                }
                if (document.querySelector('[data-test="challenge-tap-token-text"]')) return 'Fill in the Gap';
                if (document.querySelector('[data-test="challenge-text-input"]')) return 'Challenge Text Input';
                if (document.querySelector('textarea[data-test="challenge-translate-input"]')) return 'Challenge Translate Input';
                return false;
            } catch { return false; }
        },

        setValue: (el, val) => {
            const proto = el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement : window.HTMLInputElement;
            const setter = Object.getOwnPropertyDescriptor(proto.prototype, "value").set;
            setter.call(el, val);
            el.dispatchEvent(new Event('input', { bubbles: true }));
        },

        handle: async (type) => {
            const sleep = ms => new Promise(r => setTimeout(r, ms));
            try {
                switch (type) {
                    case 'Challenge Speak':
                    case 'Listen Match':
                    case 'Listen Speak':
                        document.querySelector('button[data-test="player-skip"]')?.click();
                        break;
                    case 'Select Card': {
                        const idx = window.sol.correctIndex ?? 0;
                        const cards = document.querySelectorAll('[data-test="challenge-choice-card"]');
                        if (cards.length) cards[idx]?.click();
                        else document.querySelectorAll('[data-test="challenge-choice"]')[idx]?.click();
                        break;
                    }
                    case 'Judge': {
                        const ci = window.sol.correctIndices?.[0] ?? 0;
                        document.querySelectorAll('[data-test="challenge-judge-text"]')[ci]?.click();
                        break;
                    }
                    case 'Dialogue': {
                        const idx = window.sol.correctIndex ?? 0;
                        const judges = document.querySelectorAll('[data-test="challenge-judge-text"]');
                        if (judges.length) judges[idx]?.click();
                        else document.querySelectorAll('[data-test="challenge-choice"]')[idx]?.click();
                        break;
                    }
                    case 'Translate': {
                        const { correctTokens, correctSolutions } = window.sol;
                        if (correctTokens?.length) {
                            const tokens = document.querySelectorAll('[data-test$="challenge-tap-token"]');
                            const used = [];
                            for (const word of correctTokens) {
                                for (let i = 0; i < tokens.length; i++) {
                                    if (used.includes(i) || tokens[i].disabled) continue;
                                    if (tokens[i].innerText.trim() === word.trim()) {
                                        tokens[i].click();
                                        used.push(i);
                                        await sleep(40);
                                        break;
                                    }
                                }
                            }
                        } else if (correctSolutions) {
                            const ta = document.querySelector('textarea[data-test="challenge-translate-input"]');
                            if (ta) solver.setValue(ta, correctSolutions[0]);
                        }
                        break;
                    }
                    case 'Listen Tap': {
                        const tokens = document.querySelectorAll('[data-test$="challenge-tap-token"]');
                        const used = [];
                        for (const word of (window.sol.correctTokens || [])) {
                            for (let i = 0; i < tokens.length; i++) {
                                if (used.includes(i) || tokens[i].disabled) continue;
                                if (tokens[i].innerText.trim() === word.trim()) {
                                    tokens[i].click();
                                    used.push(i);
                                    await sleep(40);
                                    break;
                                }
                            }
                        }
                        break;
                    }
                    case 'Listen Type': {
                        const ans = window.sol.prompt || window.sol.correctSolutions?.[0] || '';
                        const ta = document.querySelector('textarea[data-test="challenge-translate-input"]') || document.querySelector('[data-test="challenge-text-input"]');
                        if (ta) solver.setValue(ta, ans);
                        break;
                    }
                    case 'Complete Reverse': {
                        const blankTokens = window.sol.displayTokens?.filter(t => t.isBlank);
                        const inputs = document.querySelectorAll('[data-test="challenge-text-input"]');
                        inputs.forEach((inp, i) => { if (blankTokens?.[i]) solver.setValue(inp, blankTokens[i].text); });
                        break;
                    }
                    case 'Challenge Choice':
                        document.querySelectorAll("[data-test='challenge-choice']")[window.sol.correctIndex]?.click();
                        break;
                    case 'Challenge Choice with Text Input': {
                        const inp = document.querySelector('[data-test="challenge-text-input"]');
                        if (inp) {
                            const ans = window.sol.correctSolutions ? window.sol.correctSolutions[0].split(/(?<=^\S+)\s/)[1] : (window.sol.displayTokens?.find(t => t.isBlank)?.text || window.sol.prompt);
                            solver.setValue(inp, ans);
                        }
                        break;
                    }
                    case 'Challenge Text Input': {
                        const inp = document.querySelector('[data-test="challenge-text-input"]');
                        if (inp) solver.setValue(inp, window.sol.correctSolutions?.[0] || window.sol.displayTokens?.find(t => t.isBlank)?.text || window.sol.prompt);
                        break;
                    }
                    case 'Challenge Translate Input': {
                        const ta = document.querySelector('textarea[data-test="challenge-translate-input"]');
                        if (ta) solver.setValue(ta, window.sol.correctSolutions?.[0] || window.sol.prompt);
                        break;
                    }
                    case 'Partial Reverse': {
                        const el = document.querySelector('[data-test*="challenge-partialReverseTranslate"]')?.querySelector("span[contenteditable]");
                        if (el) {
                            const txt = window.sol?.displayTokens?.filter(t => t.isBlank)?.map(t => t.text)?.join('')?.trim() || '';
                            el.textContent = txt;
                            el.dispatchEvent(new Event('input', { bubbles: true }));
                        }
                        break;
                    }
                    case 'Type Cloze': {
                        const inp = document.querySelector('input[type="text"].b4jqk');
                        if (inp) {
                            const tok = window.sol.displayTokens.find(t => t.damageStart !== undefined);
                            if (tok) solver.setValue(inp, tok.text.slice(tok.damageStart));
                        }
                        break;
                    }
                    case 'Type Cloze Table': {
                        const rows = document.querySelectorAll('tbody tr');
                        window.sol.displayTableTokens.slice(1).forEach((rowToks, i) => {
                            const cell = rowToks[1]?.find(t => typeof t.damageStart === "number");
                            if (cell && rows[i]) {
                                const inp = rows[i].querySelector('input[type="text"].b4jqk');
                                if (inp) solver.setValue(inp, cell.text.slice(cell.damageStart));
                            }
                        });
                        break;
                    }
                    case 'Tap Cloze Table': {
                        const rows = document.querySelectorAll('tbody tr');
                        window.sol.displayTableTokens.slice(1).forEach((rowToks, i) => {
                            const cell = rowToks[1]?.find(t => typeof t.damageStart === "number");
                            if (!cell || !rows[i]) return;
                            const bank = document.querySelector('[data-test="word-bank"]');
                            const btns = bank ? Array.from(bank.querySelectorAll('button[data-test*="challenge-tap-token"]:not([aria-disabled="true"])')) : [];
                            const correct = cell.text.slice(cell.damageStart);
                            let matched = "";
                            for (const btn of btns) {
                                if (!correct.startsWith(matched + btn.innerText)) continue;
                                btn.click();
                                matched += btn.innerText;
                                if (matched === correct) break;
                            }
                        });
                        break;
                    }
                    case 'Type Complete Table': {
                        const rows = document.querySelectorAll('tbody tr');
                        window.sol.displayTableTokens.slice(1).forEach((rowToks, i) => {
                            const cell = rowToks[1]?.find(t => t.isBlank);
                            if (cell && rows[i]) {
                                const inp = rows[i].querySelector('input[type="text"].b4jqk');
                                if (inp) solver.setValue(inp, cell.text);
                            }
                        });
                        break;
                    }
                    case 'Pattern Tap Complete': {
                        const bank = document.querySelector('[data-test="word-bank"]');
                        if (bank) {
                            const idx = window.sol.correctIndex ?? 0;
                            const txt = window.sol.choices[idx];
                            const btn = Array.from(bank.querySelectorAll('button[data-test*="challenge-tap-token"]:not([aria-disabled="true"])')).find(b => b.innerText.trim() === txt);
                            if (btn) btn.click();
                        }
                        break;
                    }
                    case 'Story Arrange': {
                        const choices = document.querySelectorAll('[data-test*="challenge-tap-token"]:not(span)');
                        for (const idx of window.sol.phraseOrder) {
                            choices[idx]?.click();
                            await sleep(50);
                        }
                        break;
                    }
                    case 'Story Multiple Choice': {
                        const choices = document.querySelectorAll('[data-test="stories-choice"]');
                        choices[window.sol.correctAnswerIndex]?.click();
                        break;
                    }
                    case 'Story Point to Phrase': {
                        const choices = document.querySelectorAll('[data-test="challenge-tap-token-text"]');
                        let ci = -1;
                        for (let i = 0; i < window.sol.parts.length; i++) {
                            if (window.sol.parts[i].selectable) ci++;
                            if (window.sol.correctAnswerIndex === i) {
                                choices[ci]?.parentElement.click();
                                break;
                            }
                        }
                        break;
                    }
                    case 'Story Pairs': {
                        const btns = document.querySelectorAll('[data-test*="challenge-tap-token"]:not(span)');
                        const texts = document.querySelectorAll('[data-test="challenge-tap-token-text"]');
                        const map = new Map();
                        for (let i = 0; i < btns.length; i++) map.set(texts[i].innerText.toLowerCase().trim(), btns[i]);
                        for (const key in window.sol.dictionary) {
                            const val = window.sol.dictionary[key];
                            const k = key.split(":")[1].toLowerCase().trim();
                            const v = val.toLowerCase().trim();
                            map.get(k)?.click();
                            await sleep(50);
                            map.get(v)?.click();
                            await sleep(50);
                        }
                        break;
                    }
                    case 'Challenge Name': {
                        const arts = window.sol.articles;
                        const correct = window.sol.correctSolutions[0];
                        const match = arts.find(a => correct.startsWith(a));
                        if (match) {
                            const idx = arts.indexOf(match);
                            const sel = document.querySelector(`[data-test="challenge-choice"]:nth-child(${idx + 1})`);
                            if (sel) { sel.click(); await sleep(50); }
                            const inp = document.querySelector('[data-test="challenge-text-input"]');
                            if (inp) solver.setValue(inp, correct.substring(match.length).trim());
                        }
                        break;
                    }
                    case 'Pairs': {
                        const btns = document.querySelectorAll('[data-test*="challenge-tap-token"]:not(span)');
                        const texts = document.querySelectorAll('[data-test="challenge-tap-token-text"]');
                        if (texts.length !== btns.length || !btns.length) break;
                        for (const pair of window.sol.pairs || []) {
                            for (let i = 0; i < btns.length; i++) {
                                if (btns[i].disabled) continue;
                                const t = texts[i].innerText.toLowerCase().trim();
                                if (t === pair.transliteration?.toLowerCase().trim() || t === pair.character?.toLowerCase().trim() || t === pair.learningToken?.toLowerCase().trim() || t === pair.fromToken?.toLowerCase().trim()) {
                                    btns[i].click();
                                    await sleep(50);
                                }
                            }
                        }
                        break;
                    }
                    case 'Tokens Run': {
                        const all = document.querySelectorAll('[data-test$="challenge-tap-token"]');
                        const clicked = [];
                        for (const ct of window.sol.correctTokens) {
                            const match = Array.from(all).filter(el => el.textContent.trim() === ct.trim());
                            const idx = clicked.filter(t => t.textContent.trim() === ct.trim()).length;
                            const btn = match[idx] || match[0];
                            if (btn && !btn.disabled) { btn.click(); clicked.push(btn); }
                        }
                        break;
                    }
                    case 'Indices Run':
                    case 'Fill in the Gap': {
                        if (!window.sol.correctIndices) break;
                        const bank = document.querySelector('div[data-test="word-bank"]') || document.querySelector('.eSgkc');
                        if (!bank) break;
                        const btns = Array.from(bank.querySelectorAll('button[data-test*="challenge-tap-token"]:not(span)'));
                        for (const idx of window.sol.correctIndices) {
                            if (idx >= 0 && idx < btns.length && !btns[idx].disabled && btns[idx].getAttribute('aria-disabled') !== 'true') {
                                btns[idx].click();
                                await sleep(50);
                            }
                        }
                        break;
                    }
                    case 'Tap Complete Table': {
                        const rows = document.querySelectorAll('tbody tr');
                        const solRows = window.sol.displayTableTokens.slice(1);
                        const bank = document.querySelector('div[data-test="word-bank"]');
                        const bankBtns = bank ? Array.from(bank.querySelectorAll('button[data-test*="-challenge-tap-token"]')) : [];
                        const used = new Set();
                        for (let ri = 0; ri < solRows.length; ri++) {
                            const cell = solRows[ri][1]?.find(t => t.isBlank);
                            if (!cell) continue;
                            const txt = cell.text;
                            const rowBtns = rows[ri]?.querySelectorAll('button[data-test*="-challenge-tap-token"]');
                            let clicked = false;
                            if (rowBtns) {
                                for (const btn of rowBtns) {
                                    const tEl = btn.querySelector('[data-test="challenge-tap-token-text"]');
                                    if (tEl?.innerText.trim() === txt && !btn.disabled) { btn.click(); await sleep(50); clicked = true; break; }
                                }
                            }
                            if (!clicked && bankBtns.length) {
                                for (let i = 0; i < bankBtns.length; i++) {
                                    if (used.has(i)) continue;
                                    const tEl = bankBtns[i].querySelector('[data-test="challenge-tap-token-text"]');
                                    if (tEl?.innerText.trim() === txt && !bankBtns[i].disabled) { bankBtns[i].click(); await sleep(50); used.add(i); break; }
                                }
                            }
                        }
                        break;
                    }
                    case 'Syllable Tap':
                    case 'Syllable Listen Tap': {
                        const indices = window.sol.correctIndices;
                        const choices = window.sol.choices;
                        const domBtns = Array.from(document.querySelectorAll('[data-test="word-bank"] [data-test$="challenge-tap-token"]'));
                        indices.forEach(idx => {
                            const txt = choices[idx].text;
                            const btn = domBtns.find(b => b.innerText.trim() === txt);
                            if (btn) btn.click();
                        });
                        break;
                    }
                    case 'Character Write Drag': {
                        const createEvent = (t, x, y, btns) => new MouseEvent(t, { bubbles: true, clientX: x, clientY: y, buttons: btns, button: 0 });
                        const norm = s => s ? s.replace(/\s/g, '') : '';
                        for (const stroke of window.sol.strokes) {
                            const target = norm(stroke.path);
                            let path, handle;
                            while (!path || !handle) {
                                const cands = document.querySelectorAll('path._1e5Zt');
                                path = Array.from(cands).find(p => norm(p.getAttribute('d')) === target);
                                handle = document.querySelector('g._25Ktp');
                                if (!path || !handle) await sleep(10);
                            }
                            const mat = path.getScreenCTM(), len = path.getTotalLength();
                            const start = path.getPointAtLength(0).matrixTransform(mat);
                            const end = path.getPointAtLength(len).matrixTransform(mat);
                            handle.dispatchEvent(createEvent('mousedown', start.x, start.y, 1));
                            for (let s = 1; s <= 10; s++) {
                                const p = path.getPointAtLength((s / 10) * len).matrixTransform(mat);
                                const m = createEvent('mousemove', p.x, p.y, 1);
                                handle.dispatchEvent(m); document.dispatchEvent(m);
                            }
                            const fm = createEvent('mousemove', end.x, end.y, 1);
                            handle.dispatchEvent(fm); document.dispatchEvent(fm);
                            await sleep(5);
                            handle.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
                            document.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
                        }
                        break;
                    }
                    case 'Character Write Draw': {
                        const createEvent = (t, x, y, btns) => new MouseEvent(t, { bubbles: true, clientX: x, clientY: y, buttons: btns, button: 0 });
                        const norm = s => s ? s.replace(/\s/g, '') : '';
                        for (const stroke of window.sol.strokes) {
                            const target = norm(stroke.path);
                            let path, cursor;
                            while (!path || !cursor) {
                                const cands = document.querySelectorAll('path._1e5Zt');
                                path = Array.from(cands).find(p => norm(p.getAttribute('d')) === target);
                                cursor = document.querySelector('g._1h31R:not(._25Ktp)');
                                if (!path || !cursor) await sleep(10);
                            }
                            const mat = path.getScreenCTM(), len = path.getTotalLength();
                            const start = path.getPointAtLength(0).matrixTransform(mat);
                            const end = path.getPointAtLength(len).matrixTransform(mat);
                            cursor.dispatchEvent(createEvent('mousedown', start.x, start.y, 1));
                            document.dispatchEvent(createEvent('mousedown', start.x, start.y, 1));
                            for (let s = 1; s <= 10; s++) {
                                const p = path.getPointAtLength((s / 10) * len).matrixTransform(mat);
                                const m = createEvent('mousemove', p.x, p.y, 1);
                                cursor.dispatchEvent(m); document.dispatchEvent(m);
                            }
                            const fm = createEvent('mousemove', end.x, end.y, 1);
                            cursor.dispatchEvent(fm); document.dispatchEvent(fm);
                            await sleep(5);
                            cursor.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
                            document.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
                        }
                        break;
                    }
                    case 'Character Write Freehand': {
                        const createEvent = (t, x, y, btns) => new MouseEvent(t, { bubbles: true, clientX: x, clientY: y, buttons: btns, button: 0 });
                        const norm = s => s ? s.replace(/\s/g, '') : '';
                        const freeStrokes = window.sol.strokes.filter(s => s.strokeDrawMode === 'FREEHAND');
                        for (const stroke of freeStrokes) {
                            const target = norm(stroke.path);
                            let path, svg;
                            while (!path || !svg) {
                                const cands = document.querySelectorAll('path._22UPm');
                                path = Array.from(cands).find(p => norm(p.getAttribute('d')) === target);
                                svg = document.querySelector('svg.o1rqi');
                                if (!path || !svg) await sleep(10);
                            }
                            const mat = path.getScreenCTM(), len = path.getTotalLength();
                            const start = path.getPointAtLength(0).matrixTransform(mat);
                            const end = path.getPointAtLength(len).matrixTransform(mat);
                            svg.dispatchEvent(createEvent('mousedown', start.x, start.y, 1));
                            document.dispatchEvent(createEvent('mousedown', start.x, start.y, 1));
                            for (let s = 1; s <= 10; s++) {
                                const p = path.getPointAtLength((s / 10) * len).matrixTransform(mat);
                                const m = createEvent('mousemove', p.x, p.y, 1);
                                svg.dispatchEvent(m); document.dispatchEvent(m);
                            }
                            const fm = createEvent('mousemove', end.x, end.y, 1);
                            svg.dispatchEvent(fm); document.dispatchEvent(fm);
                            await sleep(5);
                            svg.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
                            document.dispatchEvent(createEvent('mouseup', end.x, end.y, 0));
                        }
                        break;
                    }
                }
            } catch (e) { console.error('Solver error:', e); }
        },

        clickNext: () => {
            const nb = document.querySelector('[data-test="player-next"]') || document.querySelector('[data-test="stories-player-continue"]') || document.querySelector('[data-test="stories-player-done"]');
            if (nb && nb.getAttribute('aria-disabled') !== 'true' && !nb.disabled) nb.click();
        },

        solve: async () => {
            if (solver._busy) return;
            solver._busy = true;
            try {
                const skipSel = [
                    '[data-test="practice-hub-ad-no-thanks-button"]', '[data-test="plus-no-thanks"]',
                    '[data-test="story-start"]', '.vpDIE'
                ];
                skipSel.forEach(sel => document.querySelector(sel)?.click());

                let mainEl = document.querySelector('._3yE3H') || document.querySelector('[data-test="challenge"]') || document.querySelector('[class*="challenge"]');
                if (!mainEl) { solver.clickNext(); solver._busy = false; return; }

                const ri = solver.findReact(mainEl);
                window.sol = ri?.props?.currentChallenge;
                if (!window.sol) { solver.clickNext(); solver._busy = false; return; }

                const ct = solver.determineType();
                if (ct && ct !== 'Challenge Speak' && ct !== 'Listen Match' && ct !== 'Listen Speak') {
                    await Promise.race([solver.handle(ct), new Promise(r => setTimeout(r, 2000))]);
                    await new Promise(r => setTimeout(r, 80));
                } else if (ct) {
                    const skipBtn = document.querySelector('button[data-test="player-skip"]');
                    if (!skipBtn) await new Promise(r => setTimeout(r, 500));
                    document.querySelector('button[data-test="player-skip"]')?.click();
                    await new Promise(r => setTimeout(r, 150));
                }
                solver.clickNext();
                await new Promise(r => setTimeout(r, 120));
            } catch { solver.clickNext(); }
            solver._busy = false;
        },

        _busy: false,

        createUI: () => {
            if (solverUI) return;
            solverUI = document.createElement('div');
            solverUI.className = 'DC_Solver_Btn';
            solverUI.innerHTML = `
                <button class="DC_Solve_One" id="DC_Solve_One">SOLVE</button>
                <button class="DC_Solve_All" id="DC_Solve_All">SOLVE ALL</button>
            `;
            document.body.appendChild(solverUI);
            document.getElementById('DC_Solve_One').addEventListener('click', () => solver.solve());
            document.getElementById('DC_Solve_All').addEventListener('click', () => solver.toggleAuto());
            document.addEventListener('keydown', (e) => {
                if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
                    if (e.shiftKey) solver.toggleAuto();
                    else solver.solve();
                }
            });
        },

        removeUI: () => {
            if (solverUI) { solverUI.remove(); solverUI = null; }
            isAutoMode = false;
        },

        toggleAuto: () => {
            isAutoMode = !isAutoMode;
            const btn = document.getElementById('DC_Solve_All');
            if (btn) {
                btn.textContent = isAutoMode ? 'PAUSE' : 'SOLVE ALL';
                btn.style.background = isAutoMode ? 'rgb(var(--DC-red))' : 'rgb(var(--DC-gold))';
                btn.style.borderBottomColor = isAutoMode ? '#cc0000' : 'rgb(var(--DC-gold-dark))';
            }
            if (isAutoMode) {
                (async function loop() {
                    while (isAutoMode) {
                        await solver.solve();
                        await new Promise(r => setTimeout(r, 400));
                    }
                })();
            }
        }
    };

    // Auto-solver observer
    setInterval(() => {
        const inLesson = window.location.pathname.includes('/lesson') || window.location.pathname.includes('/practice');
        if (inLesson && solverEnabled) {
            setTimeout(() => solver.createUI(), 500);
        } else {
            solver.removeUI();
        }
    }, 1000);

    // ==================== FREE SUPER ====================
    function installFakeSuper() {
        if (localStorage.getItem('dc_super') !== 'true') return;
        const RX = /https:\/\/www\.duolingo\.com\/\d{4}-\d{2}-\d{2}\/users\/.+/;
        const ITEMS = { gold_subscription: { itemName: 'gold_subscription', subscriptionInfo: { vendor: 'STRIPE', renewing: true, isFamilyPlan: true, expectedExpiration: 9999999999000 } } };
        function mod(j) {
            try {
                const d = JSON.parse(j);
                d.hasPlus = true;
                if (!d.trackingProperties) d.trackingProperties = {};
                d.trackingProperties.has_item_gold_subscription = true;
                d.shopItems = { ...d.shopItems, ...ITEMS };
                return JSON.stringify(d);
            } catch { return j; }
        }
        const uw = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
        const orig = uw.fetch;
        uw.fetch = function(resource, options) {
            const url = resource instanceof Request ? resource.url : resource;
            const method = resource instanceof Request ? resource.method : (options?.method || 'GET');
            if (method.toUpperCase() === 'GET' && RX.test(url) && !url.includes('/shop-items')) {
                return orig.apply(this, arguments).then(async r => {
                    const text = await r.clone().text();
                    return new Response(mod(text), { status: r.status, statusText: r.statusText, headers: new Headers(r.headers) });
                });
            }
            return orig.apply(this, arguments);
        };
    }
    installFakeSuper();

    // ==================== EVENT HANDLERS ====================

    // --- Dark Mode ---
    const dcRoot = document.getElementById('DC_Root');
    let isDark = localStorage.getItem('dc_dark') === 'true';
    function applyDarkMode() {
        if (isDark) {
            dcRoot.classList.add('DC_Dark');
            document.getElementById('DC_Dark_Icon_Moon').style.display = 'none';
            document.getElementById('DC_Dark_Icon_Sun').style.display = 'block';
        } else {
            dcRoot.classList.remove('DC_Dark');
            document.getElementById('DC_Dark_Icon_Moon').style.display = 'block';
            document.getElementById('DC_Dark_Icon_Sun').style.display = 'none';
        }
    }
    applyDarkMode();
    document.getElementById('DC_Dark_Btn').addEventListener('click', () => {
        isDark = !isDark;
        localStorage.setItem('dc_dark', isDark ? 'true' : 'false');
        applyDarkMode();
    });

    // --- Hide/Show with eye icon ---
    document.getElementById('DC_Hide_Btn').addEventListener('click', (e) => {
        // Prevent click firing right after drag
        if (window._dcDragMoved) return;
        hidden = !hidden;
        const main = document.getElementById('DC_Main');
        const box = document.getElementById('DC_Main_Box');
        const btn = document.getElementById('DC_Hide_Btn');
        const label = document.getElementById('DC_Hide_Label');
        box.style.transition = 'opacity 0.6s cubic-bezier(0.16,1,0.32,1), filter 0.6s cubic-bezier(0.16,1,0.32,1)';
        if (hidden) {
            box.style.opacity = '0';
            box.style.filter = 'blur(8px)';
            box.style.pointerEvents = 'none';
            label.textContent = 'Show';
            btn.classList.add('DC_Hide_Btn_Hidden');
        } else {
            box.style.opacity = '';
            box.style.filter = '';
            box.style.pointerEvents = '';
            label.textContent = 'Hide';
            btn.classList.remove('DC_Hide_Btn_Hidden');
        }
        setTimeout(() => { box.style.transition = ''; }, 600);
    });

    // --- Draggable panel ---
    (function() {
        const main = document.getElementById('DC_Main');
        let dragging = false, startX, startY, origRight, origBottom;
        window._dcDragMoved = false;

        main.addEventListener('mousedown', (e) => {
            // Only drag on the hide button (the handle) or the top bar area
            const hideBtn = document.getElementById('DC_Hide_Btn');
            if (!hideBtn.contains(e.target) && e.target !== hideBtn) return;
            dragging = true;
            window._dcDragMoved = false;
            const rect = main.getBoundingClientRect();
            startX = e.clientX;
            startY = e.clientY;
            origRight = window.innerWidth - rect.right;
            origBottom = window.innerHeight - rect.bottom;
            main.classList.add('DC_Dragging');
            e.preventDefault();
        });

        document.addEventListener('mousemove', (e) => {
            if (!dragging) return;
            const dx = e.clientX - startX;
            const dy = e.clientY - startY;
            if (Math.abs(dx) > 4 || Math.abs(dy) > 4) window._dcDragMoved = true;
            const newRight = Math.max(0, Math.min(window.innerWidth - 50, origRight - dx));
            const newBottom = Math.max(0, Math.min(window.innerHeight - 50, origBottom - dy));
            main.style.right = newRight + 'px';
            main.style.bottom = newBottom + 'px';
        });

        document.addEventListener('mouseup', () => {
            if (!dragging) return;
            dragging = false;
            main.classList.remove('DC_Dragging');
            // Save position
            localStorage.setItem('dc_pos', JSON.stringify({ right: main.style.right, bottom: main.style.bottom }));
            setTimeout(() => { window._dcDragMoved = false; }, 50);
        });

        // Restore saved position
        try {
            const saved = JSON.parse(localStorage.getItem('dc_pos') || 'null');
            if (saved && saved.right && saved.bottom) {
                main.style.right = saved.right;
                main.style.bottom = saved.bottom;
            }
        } catch {}
    })();

    // XP button
    const xpI = document.getElementById('DC_XP_Input'), xpB = document.getElementById('DC_XP_Btn');
    xpI.addEventListener('input', () => { xpB.disabled = !user || !xpI.value || +xpI.value < 30; });
    xpB.addEventListener('click', () => {
        if (running && task === 'xp') { run('xp', 0); return; }
        if (running) { notif('Stop current farm first.'); return; }
        const v = +xpI.value;
        if (v < 30) { notif('Min 30 XP'); return; }
        run('xp', v);
    });
    xpI.addEventListener('keydown', e => { if (e.key === 'Enter' && !xpB.disabled) xpB.click(); });

    // Gems button
    const gmB = document.getElementById('DC_Gem_Btn');
    gmB.addEventListener('click', () => {
        if (running && task === 'gems') { run('gems', 0); return; }
        if (running) { notif('Stop current farm first.'); return; }
        run('gems', 0);
    });

    // Streak button
    const stI = document.getElementById('DC_Streak_Input'), stB = document.getElementById('DC_Streak_Btn');
    stI.addEventListener('input', () => { stB.disabled = !user || !stI.value || +stI.value < 1; });
    stB.addEventListener('click', () => {
        if (running && task === 'streak') { run('streak', 0); return; }
        if (running) { notif('Stop current farm first.'); return; }
        const v = +stI.value;
        if (v < 1) return;
        run('streak', v);
    });
    stI.addEventListener('keydown', e => { if (e.key === 'Enter' && !stB.disabled) stB.click(); });

    // Practice button
    document.getElementById('DC_Practice_Btn').addEventListener('click', () => {
        if (!window.location.pathname.startsWith('/practice')) {
            window.location.assign('/practice');
        }
    });

    // Super button
    document.getElementById('DC_Super_Btn').addEventListener('click', () => {
        localStorage.setItem('dc_super', localStorage.getItem('dc_super') === 'true' ? 'false' : 'true');
        notif(localStorage.getItem('dc_super') === 'true' ? 'Free Super activated! Reload page.' : 'Free Super deactivated. Reload page.');
    });

    // Solver toggle
    const solverT = document.getElementById('DC_Solver_Toggle');
    solverT.checked = solverEnabled;
    solverT.addEventListener('change', () => {
        solverEnabled = solverT.checked;
        localStorage.setItem('dc_solver', solverEnabled ? 'true' : 'false');
        if (!solverEnabled) solver.removeUI();
    });

    // ==================== INITIALIZATION ====================
    // Initial animation
    const main = document.getElementById('DC_Main');
    const box = document.getElementById('DC_Main_Box');
    box.style.opacity = '0';
    box.style.filter = 'blur(8px)';
    setTimeout(() => {
        box.style.transition = 'opacity 0.6s cubic-bezier(0.16,1,0.32,1), filter 0.6s cubic-bezier(0.16,1,0.32,1)';
        box.style.opacity = '';
        box.style.filter = '';
        setTimeout(() => { box.style.transition = ''; }, 600);
    }, 300);

    connect();

})();