-- ui.lua
local addonName = ...

------------------------------------------------------------
-- Tiny utilities
------------------------------------------------------------

local function withCombatGuard(fn)
    if InCombatLockdown() then
        print("|cffffff00CCS:|r Cannot change settings during combat.")
        return
    end
    fn()
end

local function stripButtonBorder(btn)
    if btn.Left   then btn.Left:Hide()   end
    if btn.Middle then btn.Middle:Hide() end
    if btn.Right  then btn.Right:Hide()  end
end

local function stripCheckBorder(cb)
    cb:SetNormalTexture("")
    cb:SetPushedTexture("")
    cb:SetHighlightTexture("")
    cb:SetDisabledTexture("")
    cb:SetCheckedTexture("Interface\\Buttons\\UI-CheckBox-Check")
    local t = cb:GetCheckedTexture()
    if t then t:SetVertexColor(0.8, 0.8, 0.8, 1) end
end

local function addTooltip(frame, title, body)
    frame:EnableMouse(true)
    frame:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine(title, 1, 1, 1)
        if body then GameTooltip:AddLine(body, 0.8, 0.8, 0.8, true) end
        GameTooltip:Show()
    end)
    frame:SetScript("OnLeave", function() GameTooltip:Hide() end)
end

-- Preview a sound by value key (LSM name or "file:name" or CCS short name)
local function previewSound(value)
    if not value or value == "__default__" then return end
    local path = CCS.ResolvePath and CCS.ResolvePath(value)
    if path then PlaySoundFile(path, "Master") end
end

local CATEGORY_NAME = "Custom Countdown Sounds"

------------------------------------------------------------
-- Dropdown widget
------------------------------------------------------------

local CCS_DropdownPopup

local function CCS_GetOrCreatePopup()
    if CCS_DropdownPopup then return CCS_DropdownPopup end

    local MAX_VISIBLE = 13
    local ROW_H    = 16
    local PAD      = 6
    local SCROLL_W = 14
    local SEARCH_H = 22
    local PREV_W   = 22

    local popup = CreateFrame("Frame", "CCS_DropdownPopup", UIParent, "BackdropTemplate")
    popup:SetFrameStrata("FULLSCREEN_DIALOG")
    popup:SetFrameLevel(200)
    popup:SetBackdrop({
        bgFile   = "Interface/Tooltips/UI-Tooltip-Background",
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        edgeSize = 12,
        insets   = { left=3, right=3, top=3, bottom=3 },
    })
    popup:SetBackdropColor(0.1, 0.1, 0.1, 0.97)
    popup:SetBackdropBorderColor(0.5, 0.5, 0.5, 1)
    popup:Hide()
    popup._buttons = {}
    popup._owner   = nil
    popup._offset  = 0

    local searchBox = CreateFrame("EditBox", nil, popup, "BackdropTemplate")
    searchBox:SetHeight(SEARCH_H)
    searchBox:SetPoint("TOPLEFT",  popup, "TOPLEFT",  PAD, -PAD)
    searchBox:SetPoint("TOPRIGHT", popup, "TOPRIGHT", -(PAD + SCROLL_W + 2), -PAD)
    searchBox:SetBackdrop({
        bgFile   = "Interface/Tooltips/UI-Tooltip-Background",
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        edgeSize = 8,
        insets   = { left=3, right=3, top=3, bottom=3 },
    })
    searchBox:SetBackdropColor(0.05, 0.05, 0.05, 1)
    searchBox:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
    searchBox:SetFontObject("GameFontHighlightSmall")
    searchBox:SetTextInsets(6, 6, 0, 0)
    searchBox:SetAutoFocus(false)
    searchBox:SetMaxLetters(64)
    searchBox:Hide()
    popup._searchBox = searchBox

    local searchHint = searchBox:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    searchHint:SetPoint("LEFT",  searchBox, "LEFT",  6, 0)
    searchHint:SetPoint("RIGHT", searchBox, "RIGHT", -6, 0)
    searchHint:SetJustifyH("LEFT")
    searchHint:SetText("Type to search...")

    local clipper = CreateFrame("Frame", nil, popup)
    clipper:SetPoint("BOTTOMRIGHT", popup, "BOTTOMRIGHT", -(PAD + SCROLL_W + 2), PAD)
    popup._clipper = clipper

    local track = CreateFrame("Frame", nil, popup, "BackdropTemplate")
    track:SetWidth(SCROLL_W)
    track:SetPoint("TOPRIGHT",    popup, "TOPRIGHT",    -PAD, -PAD)
    track:SetPoint("BOTTOMRIGHT", popup, "BOTTOMRIGHT", -PAD,  PAD)
    track:SetBackdrop({
        bgFile   = "Interface/Tooltips/UI-Tooltip-Background",
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        edgeSize = 6,
        insets   = { left=2, right=2, top=2, bottom=2 },
    })
    track:SetBackdropColor(0.05, 0.05, 0.05, 1)
    track:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)

    local thumb = track:CreateTexture(nil, "OVERLAY")
    thumb:SetColorTexture(0.5, 0.5, 0.5, 0.8)
    popup._thumb = thumb

    local function UpdateThumb()
        local total   = popup._total or 0
        local visible = math.min(total, MAX_VISIBLE)
        if total <= visible then thumb:Hide(); return end
        thumb:Show()
        local trackH = track:GetHeight()
        local ratio  = visible / total
        local thumbH = math.max(16, trackH * ratio)
        local travel = trackH - thumbH
        local frac   = (popup._offset or 0) / math.max(1, total - visible)
        thumb:SetHeight(thumbH)
        thumb:ClearAllPoints()
        thumb:SetPoint("TOPLEFT",  track, "TOPLEFT",  2, -(travel * frac))
        thumb:SetPoint("TOPRIGHT", track, "TOPRIGHT", -2, -(travel * frac))
    end

    local function Refresh()
        local items  = popup._items or {}
        local owner  = popup._owner
        local offset = popup._offset or 0
        local wide   = owner and owner._widePreview
        for i = 1, MAX_VISIBLE do
            local row  = popup._buttons[i]
            local item = items[offset + i]
            if item then
                row._text:ClearAllPoints()
                row._text:SetPoint("LEFT", row._check, "RIGHT", 2, 0)
                if wide then
                    row._text:SetPoint("RIGHT", row, "RIGHT", -(PREV_W + 6), 0)
                    row._prev:Show()
                else
                    row._text:SetPoint("RIGHT", row, "RIGHT", -4, 0)
                    row._prev:Hide()
                end
                row._text:SetText(item.label)
                row._check:SetText(item.value == (owner and owner._value) and "|cff00ff00*|r" or "")
                row:ClearAllPoints()
                row:SetPoint("TOPLEFT",  clipper, "TOPLEFT",  0, -(i-1)*ROW_H)
                row:SetPoint("TOPRIGHT", clipper, "TOPRIGHT", 0, -(i-1)*ROW_H)
                row:Show()
                row:SetScript("OnClick", function()
                    if owner then
                        owner._value = item.value
                        owner._label:SetText(item.shortLabel or item.label)
                        if item.value == "__default__" then
                            owner:SetBackdropColor(0.15, 0.15, 0.15, 1)
                        else
                            owner:SetBackdropColor(0.05, 0.28, 0.05, 1)
                        end
                        if owner._onSelect then owner._onSelect(item.value) end
                    end
                    popup:Hide()
                end)
                row._prev:SetScript("OnClick", function()
                    previewSound(item.value == "__default__" and popup._default or item.value)
                end)
            else
                row:Hide()
            end
        end
        UpdateThumb()
    end
    popup._refresh = Refresh

    searchBox:SetScript("OnTextChanged", function(self)
        local txt = self:GetText()
        searchHint:SetShown(txt == "")
        local query = txt:lower()
        if query == "" then
            popup._items = popup._allItems
        else
            local filtered = {}
            for _, item in ipairs(popup._allItems) do
                if item.label:lower():find(query, 1, true) then
                    filtered[#filtered + 1] = item
                end
            end
            popup._items = filtered
        end
        popup._offset = 0
        popup._total  = #popup._items
        Refresh()
    end)
    searchBox:SetScript("OnEscapePressed", function() popup:Hide() end)

    popup:EnableMouseWheel(true)
    popup:SetScript("OnMouseWheel", function(_, delta)
        local total   = popup._total or 0
        local visible = math.min(total, MAX_VISIBLE)
        popup._offset = math.max(0, math.min(popup._offset - delta, total - visible))
        Refresh()
    end)

    track:EnableMouse(true)
    track:SetScript("OnMouseDown", function(_, btn)
        if btn ~= "LeftButton" then return end
        local total   = popup._total or 0
        local visible = math.min(total, MAX_VISIBLE)
        if total <= visible then return end
        track:SetScript("OnUpdate", function()
            local _, my = GetCursorPosition()
            local scale = track:GetEffectiveScale()
            local frac  = math.max(0, math.min(1, (track:GetTop() - my/scale) / track:GetHeight()))
            popup._offset = math.floor(frac * (total - visible) + 0.5)
            Refresh()
        end)
    end)
    track:SetScript("OnMouseUp", function() track:SetScript("OnUpdate", nil) end)

    for i = 1, MAX_VISIBLE do
        local row = CreateFrame("Button", nil, clipper)
        row:SetHeight(ROW_H)
        row._check = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        row._check:SetPoint("LEFT", row, "LEFT", 6, 0)
        row._check:SetWidth(14)
        row._check:SetJustifyH("LEFT")
        row._text = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        row._text:SetPoint("LEFT", row._check, "RIGHT", 2, 0)
        row._text:SetJustifyH("LEFT")
        local hl = row:CreateTexture(nil, "HIGHLIGHT")
        hl:SetAllPoints(); hl:SetColorTexture(1, 1, 1, 0.1)
        local prev = CreateFrame("Button", nil, row)
        prev:SetSize(PREV_W, ROW_H)
        prev:SetPoint("RIGHT", row, "RIGHT", -2, 0)
        local prevHL = prev:CreateTexture(nil, "HIGHLIGHT")
        prevHL:SetAllPoints(); prevHL:SetColorTexture(0.6, 0.8, 1, 0.15)
        local prevTex = prev:CreateTexture(nil, "ARTWORK")
        prevTex:SetSize(PREV_W - 6, PREV_W - 6)
        prevTex:SetPoint("CENTER")
        prevTex:SetAtlas("common-icon-sound")
        prevTex:SetVertexColor(0.6, 0.8, 1, 0.7)
        prev:SetScript("OnEnter", function() prevTex:SetVertexColor(0.8, 1, 1, 1) end)
        prev:SetScript("OnLeave", function() prevTex:SetVertexColor(0.6, 0.8, 1, 0.7) end)
        row._prev = prev
        row:Hide()
        popup._buttons[i] = row
    end

    local catcher = CreateFrame("Frame", nil, UIParent)
    catcher:SetAllPoints(UIParent)
    catcher:SetFrameStrata("FULLSCREEN_DIALOG")
    catcher:SetFrameLevel(199)
    catcher:Hide()
    catcher:EnableMouse(true)
    catcher:SetScript("OnMouseDown", function() popup:Hide() end)
    popup._catcher = catcher

    popup:SetScript("OnHide", function()
        catcher:Hide()
        searchBox:SetText("")
        searchBox:ClearFocus()
    end)

    CCS_DropdownPopup = popup
    return popup
end

local function CCS_CreateDropdown(parent, width, height)
    local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
    btn:SetSize(width or 120, height or 22)
    btn:SetBackdrop({
        bgFile   = "Interface/Tooltips/UI-Tooltip-Background",
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        edgeSize = 10,
        insets   = { left=3, right=3, top=3, bottom=3 },
    })
    btn:SetBackdropColor(0.15, 0.15, 0.15, 1)
    btn:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)

    btn._label = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    btn._label:SetPoint("LEFT",  btn, "LEFT",  8,   0)
    btn._label:SetPoint("RIGHT", btn, "RIGHT", -18, 0)
    btn._label:SetJustifyH("LEFT")
    btn._label:SetText("--")

    local arrow = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    arrow:SetPoint("RIGHT", btn, "RIGHT", -6, 0)
    arrow:SetText("v")
    arrow:SetTextColor(0.8, 0.8, 0.8)
    btn._arrow = arrow

    btn._items    = {}
    btn._value    = nil
    btn._onSelect = nil
    btn._enabled  = true

    btn:SetScript("OnEnter", function(self)
        if self._enabled then self:SetBackdropBorderColor(0.8, 0.8, 0.8, 1) end
    end)
    btn:SetScript("OnLeave", function(self)
        if self._enabled then self:SetBackdropBorderColor(0.4, 0.4, 0.4, 1) end
    end)
    btn:SetScript("OnClick", function(self)
        if not self._enabled then return end
        local popup = CCS_GetOrCreatePopup()
        if popup:IsShown() and popup._owner == self then popup:Hide(); return end

        local MAX_VISIBLE = 13
        local ROW_H    = 16
        local PAD      = 6
        local SCROLL_W = 14
        local SEARCH_H = 22
        local hasSearch = self._widePreview

        popup._owner    = self
        popup._allItems = self._items
        popup._items    = self._items
        popup._offset   = 0
        popup._total    = #self._items
        popup._default  = self._defaultSound

        if hasSearch then
            popup._searchBox:Show()
            popup._searchBox:SetText("")
            popup._clipper:SetPoint("TOPLEFT", popup, "TOPLEFT", PAD, -(PAD + SEARCH_H + 4))
        else
            popup._searchBox:Hide()
            popup._clipper:SetPoint("TOPLEFT", popup, "TOPLEFT", PAD, -PAD)
        end

        local visible = math.min(#self._items, MAX_VISIBLE)
        local pw = hasSearch and (math.max(width * 2, 260) + SCROLL_W + PAD)
                             or  ((self._popupWidth or width or 110) + SCROLL_W + PAD)
        local extraH = hasSearch and (SEARCH_H + 4) or 0
        popup:SetSize(pw, PAD*2 + visible*ROW_H + extraH)
        popup:ClearAllPoints()
        popup:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 0, -2)
        popup._refresh()
        popup:Show()
        popup._catcher:Show()
        if hasSearch then popup._searchBox:SetFocus() end
    end)

    function btn:SetItems(items)  self._items = items or {} end
    function btn:GetValue()       return self._value end
    function btn:SetOnSelect(fn)  self._onSelect = fn end

    function btn:SetValue(value)
        self._value = value
        for _, item in ipairs(self._items) do
            if item.value == value then
                self._label:SetText(item.shortLabel or item.label)
                if value == "__default__" then
                    self:SetBackdropColor(0.15, 0.15, 0.15, 1)
                else
                    self:SetBackdropColor(0.05, 0.28, 0.05, 1)
                end
                return
            end
        end
        self._label:SetText("--")
        self:SetBackdropColor(0.15, 0.15, 0.15, 1)
    end

    function btn:SetEnabled(enabled)
        self._enabled = enabled and true or false
        if self._enabled then
            self._label:SetTextColor(1,   1,   1)
            self._arrow:SetTextColor(0.8, 0.8, 0.8)
            local isOverride = self._value and self._value ~= "__default__"
            self:SetBackdropColor(isOverride and 0.05 or 0.15, isOverride and 0.28 or 0.15, isOverride and 0.05 or 0.15, 1)
            self:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
        else
            self._label:SetTextColor(0.5, 0.5, 0.5)
            self._arrow:SetTextColor(0.4, 0.4, 0.4)
            self:SetBackdropColor(0.1, 0.1, 0.1, 1)
            self:SetBackdropBorderColor(0.25, 0.25, 0.25, 1)
        end
    end

    return btn
end

------------------------------------------------------------
-- Sound / item helpers
------------------------------------------------------------

local COUNTDOWN_KEYS = {
    "file:ccs2s","file:ccs3s","file:ccs4s","file:ccs5s","file:ccs6s","file:ccs7s",
    "file:ccs8s","file:ccs9s","file:ccs10s","file:ccs11s","file:ccs12s","file:ccs13s",
}
local COUNTDOWN_LABELS = {
    ["file:ccs2s"]="2 Seconds",  ["file:ccs3s"]="3 Seconds",  ["file:ccs4s"]="4 Seconds",
    ["file:ccs5s"]="5 Seconds",  ["file:ccs6s"]="6 Seconds",  ["file:ccs7s"]="7 Seconds",
    ["file:ccs8s"]="8 Seconds",  ["file:ccs9s"]="9 Seconds",  ["file:ccs10s"]="10 Seconds",
    ["file:ccs11s"]="11 Seconds",["file:ccs12s"]="12 Seconds",["file:ccs13s"]="13 Seconds",
}

local function prettifyKey(key)
    if not key then return "none" end
    local s = key:gsub("^file:", ""):gsub("_", " ")
    return s:gsub("(%a)([%w]*)", function(a, b) return a:upper() .. b:lower() end)
end

local function getDefaultWarn(ability)
    local s = ability.soundH or ability.soundM
    if not s then return nil end
    return type(s) == "table" and s[1] or s
end

local function getDefaultCountdown(ability, diff)
    local s = diff == "M" and ability.soundM or ability.soundH
    return type(s) == "table" and s[2] or nil
end

local function hasHeroic(ability) return ability.soundH ~= nil or CCS.GetCountdownOverride(ability.key, "H") ~= nil end
local function hasMythic(ability)  return ability.soundM ~= nil or CCS.GetCountdownOverride(ability.key, "M") ~= nil end

-- Cached sound + item lists — never recreated unless LSM changes
local cachedSoundItems    = nil
local _warnItemsCache     = {}   -- defaultKey -> item list
local _countdownItemsCache = {}  -- defaultKey|"__nil__" -> item list

local function getSoundItems()
    if cachedSoundItems then return cachedSoundItems end
    local LSM   = LibStub and LibStub("LibSharedMedia-3.0", true)
    local items = {}
    if LSM then
        local list = LSM:List("sound") or {}
        table.sort(list)
        for _, name in ipairs(list) do
            items[#items + 1] = { label = name, value = name }
        end
    end
    cachedSoundItems = items
    return items
end

local function buildWarnItems(defaultKey)
    local k = defaultKey or "__nil__"
    if _warnItemsCache[k] then return _warnItemsCache[k] end
    local short = prettifyKey(defaultKey)
    local items = {{ label = short .. " (default)", shortLabel = short, value = "__default__" }}
    for _, item in ipairs(getSoundItems()) do
        items[#items + 1] = { label = item.label, shortLabel = item.label, value = item.value }
    end
    _warnItemsCache[k] = items
    return items
end

local function buildCountdownItems(defaultKey)
    local k = defaultKey or "__nil__"
    if _countdownItemsCache[k] then return _countdownItemsCache[k] end
    local shortDef = defaultKey and (COUNTDOWN_LABELS[defaultKey] or prettifyKey(defaultKey)) or "off"
    local items = {{ label = shortDef .. " (default)", shortLabel = shortDef:match("^(%d+)") or shortDef, value = "__default__" }}
    for _, ck in ipairs(COUNTDOWN_KEYS) do
        local full = COUNTDOWN_LABELS[ck]
        local num  = full:match("^(%d+)") or full
        items[#items + 1] = { label = num, shortLabel = num, value = ck }
    end
    _countdownItemsCache[k] = items
    return items
end

------------------------------------------------------------
-- Layout constants
------------------------------------------------------------

local ROW_HEIGHT = 22
local HEADER_H   = 24
local INDENT     = 12
local CB_SIZE    = 22
local DD_W       = 110   -- warn dropdown width
local CD_W       = 50    -- countdown dropdown width
local LEFT_FRAC  = 0.50

-- Column offsets (computed once from constants, reused everywhere)
local rPad   = 10
local colHCB = rPad + 12
local colHDD = colHCB + CB_SIZE + 2
local colMCB = colHDD + CD_W + 24
local colMDD = colMCB + CB_SIZE + 2
local colCB  = rPad + 12
local colDD  = colCB  + CB_SIZE + 2

-- Raid header colors (file-scope constant)
local RAID_COLORS = {
    ["March on Quel'Danas"] = "|cff6fcf6f",
    ["The Dreamrift"]       = "|cff6aacdc",
    ["The Voidspire"]       = "|cffc17de8",
}

------------------------------------------------------------
-- Frame pool
------------------------------------------------------------

local _pool = { rows={}, headers={}, raidBgs={}, seps={}, divider=nil }

local function acquireRow(scrollChild, idx)
    if _pool.rows[idx] then return _pool.rows[idx] end

    local leftCell  = CreateFrame("Frame", nil, scrollChild)
    local rightCell = CreateFrame("Frame", nil, scrollChild)

    local warnDD = CCS_CreateDropdown(leftCell, DD_W, ROW_HEIGHT - 4)
    warnDD:SetPoint("RIGHT", leftCell, "RIGHT", -8, 0)
    warnDD._widePreview = true

    local warnCB = CreateFrame("CheckButton", nil, leftCell, "UICheckButtonTemplate")
    warnCB:SetSize(CB_SIZE, CB_SIZE)
    warnCB:SetPoint("RIGHT", warnDD, "LEFT", -4, 0)
    stripCheckBorder(warnCB)

    local warnNoLbl = leftCell:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
    warnNoLbl:SetText("|cff555555No default|r")
    warnNoLbl:SetPoint("RIGHT", leftCell, "RIGHT", -8, 0)

    local icon = leftCell:CreateTexture(nil, "ARTWORK")
    icon:SetSize(18, 18)
    icon:SetPoint("LEFT", leftCell, "LEFT", INDENT, 0)

    local lblFrame = CreateFrame("Frame", nil, leftCell)
    lblFrame:SetPoint("LEFT",  leftCell, "LEFT", INDENT + 22, 0)
    lblFrame:SetPoint("RIGHT", warnCB,   "LEFT", -6, 0)
    lblFrame:SetHeight(ROW_HEIGHT)
    local lbl = lblFrame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    lbl:SetAllPoints(); lbl:SetJustifyH("LEFT"); lbl:SetJustifyV("MIDDLE")

    -- Raid right-cell controls
    local hLbl = rightCell:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
    hLbl:SetText("|cffaaaaaa HC|r")
    local hCB = CreateFrame("CheckButton", nil, rightCell, "UICheckButtonTemplate")
    hCB:SetSize(CB_SIZE, CB_SIZE); stripCheckBorder(hCB)
    local hDD = CCS_CreateDropdown(rightCell, CD_W, ROW_HEIGHT - 4)
    hDD._popupWidth = 130
    local hNoLbl = rightCell:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
    hNoLbl:SetText("|cff555555No default|r")
    local mLbl = rightCell:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
    mLbl:SetText("|cffaaaaaa M|r")
    local mCB = CreateFrame("CheckButton", nil, rightCell, "UICheckButtonTemplate")
    mCB:SetSize(CB_SIZE, CB_SIZE); stripCheckBorder(mCB)
    local mDD = CCS_CreateDropdown(rightCell, CD_W, ROW_HEIGHT - 4)
    mDD._popupWidth = 130
    local mNoLbl = rightCell:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
    mNoLbl:SetText("|cff555555No default|r")

    -- M+ right-cell controls
    local cdCB = CreateFrame("CheckButton", nil, rightCell, "UICheckButtonTemplate")
    cdCB:SetSize(CB_SIZE, CB_SIZE); stripCheckBorder(cdCB)
    local cdDD = CCS_CreateDropdown(rightCell, CD_W, ROW_HEIGHT - 4)
    cdDD._popupWidth = 130
    local cdNoLbl = rightCell:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
    cdNoLbl:SetText("|cff555555No default|r")

    -- Row state — mutated by rebind, read by permanent callbacks
    local r = {
        leftCell=leftCell, rightCell=rightCell,
        warnDD=warnDD, warnCB=warnCB, warnNoLbl=warnNoLbl, icon=icon, lbl=lbl, lblFrame=lblFrame,
        hLbl=hLbl, hCB=hCB, hDD=hDD, hNoLbl=hNoLbl,
        mLbl=mLbl, mCB=mCB, mDD=mDD, mNoLbl=mNoLbl,
        cdCB=cdCB, cdDD=cdDD, cdNoLbl=cdNoLbl,
        _ability=nil, _isMplus=false,
        _hDefaultCD=nil, _mDefaultCD=nil, _cdDefaultCD=nil,
        _hOver=nil, _mOver=nil, _cdOver=nil,
        _warnNoDefault=false,
    }

    -- Permanent visibility refreshers (read r._ state, no ability captures)
    local function refreshWarnDD()
        local a = r._ability; if not a then return end
        local en = warnCB:GetChecked()
        if not r._warnNoDefault or en or CCS.GetWarnOverride(a.key) then
            warnDD:Show(); warnDD:SetEnabled(en); warnNoLbl:Hide()
        else
            warnDD:Hide(); warnNoLbl:Show()
        end
    end
    local function refreshHDD()
        local a = r._ability
        if not a then hLbl:Hide(); hCB:Hide(); hDD:Hide(); hNoLbl:Hide(); return end
        local en = hCB:GetChecked()
        if en or r._hDefaultCD or r._hOver then
            hDD:Show(); hDD:SetEnabled(en); hNoLbl:Hide()
        else
            hDD:Hide(); hNoLbl:Show()
        end
        hLbl:Show(); hCB:Show()
    end
    local function refreshMDD()
        local a = r._ability
        if not a then mLbl:Hide(); mCB:Hide(); mDD:Hide(); mNoLbl:Hide(); return end
        local en = mCB:GetChecked()
        if en or r._mDefaultCD or r._mOver then
            mDD:Show(); mDD:SetEnabled(en); mNoLbl:Hide()
        else
            mDD:Hide(); mNoLbl:Show()
        end
        mLbl:Show(); mCB:Show()
    end
    local function refreshCdDD()
        local a = r._ability
        if not a then cdCB:Hide(); cdDD:Hide(); cdNoLbl:Hide(); return end
        local en = cdCB:GetChecked()
        if en or r._cdDefaultCD or r._cdOver then cdDD:Show(); cdDD:SetEnabled(en); cdNoLbl:Hide()
        else cdDD:Hide(); cdNoLbl:Show() end
        cdCB:Show()
    end
    r.refreshHDD  = refreshHDD
    r.refreshMDD  = refreshMDD
    r.refreshCdDD = refreshCdDD

    -- Wire tooltips (permanent, once per row)
    addTooltip(warnDD,  "Warning Sound",          "Choose which sound plays when this aura is applied.")
    addTooltip(warnCB,  "Warning Sound",          "Play a sound when this ability's aura is applied to you.")
    addTooltip(hCB,     "Heroic Countdown",       "Play a countdown sound on Heroic difficulty.")
    addTooltip(hDD,     "Heroic Countdown Timer", "Choose the countdown timer for Heroic difficulty.")
    addTooltip(mCB,     "Mythic Countdown",       "Play a countdown sound on Mythic difficulty.")
    addTooltip(mDD,     "Mythic Countdown Timer", "Choose the countdown timer for Mythic difficulty.")
    addTooltip(cdCB,    "Countdown",              "Play a countdown sound when this aura is applied.")
    addTooltip(cdDD,    "Countdown Timer",        "Choose the countdown timer in seconds.")

    lblFrame:EnableMouse(true)
    lblFrame:SetScript("OnEnter", function(self)
        local a = r._ability
        if not a or not a.privateID then return end
        GameTooltip:SetOwner(self, "ANCHOR_NONE")
        GameTooltip:SetPoint("TOPRIGHT", self:GetParent(), "TOPLEFT", 0, 0)
        GameTooltip:SetSpellByID(a.privateID)
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("|cffaaaaaa SpellID: " .. a.privateID .. "|r")
        GameTooltip:Show()
    end)
    lblFrame:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Permanent action callbacks — capture frame locals, read r._ability at call time
    warnCB:SetScript("OnClick", function(self)
        withCombatGuard(function()
            local a = r._ability; if not a then return end
            local en = self:GetChecked()
            CCS.SetWarnEnabled(a.key, en)
            CCS.RefreshAbility(a.key, a)
            warnDD:SetEnabled(en)
            refreshWarnDD()
        end)
    end)
    warnDD:SetOnSelect(function(v)
        withCombatGuard(function()
            local a = r._ability; if not a then return end
            CCS.SetWarnOverride(a.key, v ~= "__default__" and v or nil)
            CCS.RefreshAbility(a.key, a)
        end)
    end)
    hCB:SetScript("OnClick", function(self)
        withCombatGuard(function()
            local a = r._ability; if not a then return end
            CCS.SetCDEnabled(a.key, "H", self:GetChecked())
            CCS.RefreshAbility(a.key, a); refreshHDD()
        end)
    end)
    hDD:SetOnSelect(function(v)
        withCombatGuard(function()
            local a = r._ability; if not a then return end
            r._hOver = v ~= "__default__" and v or nil
            CCS.SetCountdownOverride(a.key, "H", r._hOver)
            CCS.RefreshAbility(a.key, a); refreshHDD()
        end)
    end)
    mCB:SetScript("OnClick", function(self)
        withCombatGuard(function()
            local a = r._ability; if not a then return end
            CCS.SetCDEnabled(a.key, "M", self:GetChecked())
            CCS.RefreshAbility(a.key, a); refreshMDD()
        end)
    end)
    mDD:SetOnSelect(function(v)
        withCombatGuard(function()
            local a = r._ability; if not a then return end
            r._mOver = v ~= "__default__" and v or nil
            CCS.SetCountdownOverride(a.key, "M", r._mOver)
            CCS.RefreshAbility(a.key, a); refreshMDD()
        end)
    end)
    cdCB:SetScript("OnClick", function(self)
        withCombatGuard(function()
            local a = r._ability; if not a then return end
            CCS.SetCDEnabled(a.key, "M", self:GetChecked())
            CCS.RefreshAbility(a.key, a); refreshCdDD()
        end)
    end)
    cdDD:SetOnSelect(function(v)
        withCombatGuard(function()
            local a = r._ability; if not a then return end
            r._cdOver = v ~= "__default__" and v or nil
            CCS.SetCountdownOverride(a.key, "M", r._cdOver)
            CCS.RefreshAbility(a.key, a); refreshCdDD()
        end)
    end)

    -- rebind: zero allocations — updates row to show a new ability
    function r.rebind(ability, isMplus)
        r._ability = ability
        r._isMplus = isMplus

        local texID = C_Spell.GetSpellTexture(ability.privateID)
        if texID then icon:SetTexture(texID); icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        else          icon:SetColorTexture(0.2, 0.2, 0.2, 0.5) end
        lbl:SetText(ability.label)
        if ability.privateID then C_Spell.RequestLoadSpellData(ability.privateID) end

        -- Warn controls (same for both modes)
        local defaultWarn = getDefaultWarn(ability)
        r._warnNoDefault = (defaultWarn == nil)
        r._warnCtrl.abilityKey = ability.key
        warnDD:SetItems(buildWarnItems(defaultWarn))
        warnDD._defaultWarn  = defaultWarn
        warnDD._defaultSound = defaultWarn
        warnDD._abilityKey   = ability.key
        warnDD:SetValue(CCS.GetWarnOverride(ability.key) or "__default__")
        local warnEn = CCS.isWarnEnabled(ability.key)
        warnCB:SetChecked(warnEn)
        refreshWarnDD()

        if isMplus then
            hLbl:Hide(); hCB:Hide(); hDD:Hide(); hNoLbl:Hide()
            mLbl:Hide(); mCB:Hide(); mDD:Hide(); mNoLbl:Hide()

            cdCB:ClearAllPoints(); cdCB:SetPoint("LEFT", rightCell, "LEFT", colCB, 0)
            cdDD:ClearAllPoints(); cdDD:SetPoint("LEFT", rightCell, "LEFT", colDD, 0)
            cdNoLbl:ClearAllPoints(); cdNoLbl:SetPoint("LEFT", rightCell, "LEFT", colDD, 0)

            r._cdDefaultCD = getDefaultCountdown(ability, "M") or getDefaultCountdown(ability, "H")
            r._cdOver      = CCS.GetCountdownOverride(ability.key, "M")
            r._cdCtrl.abilityKey = ability.key
            cdCB:SetChecked(CCS.IsCDEnabled(ability.key, "M"))
            cdDD:SetItems(buildCountdownItems(r._cdDefaultCD))
            cdDD._defaultSound = r._cdDefaultCD
            cdDD:SetValue(r._cdOver or "__default__")
            cdCB:Show(); cdDD:Show(); cdNoLbl:Show()
            refreshCdDD()
        else
            cdCB:Hide(); cdDD:Hide(); cdNoLbl:Hide()

            hLbl:ClearAllPoints(); hLbl:SetPoint("LEFT", rightCell, "LEFT", colHCB - 20, 0)
            hCB:ClearAllPoints();  hCB:SetPoint( "LEFT", rightCell, "LEFT", colHCB, 0)
            hDD:ClearAllPoints();  hDD:SetPoint( "LEFT", rightCell, "LEFT", colHDD, 0)
            hNoLbl:ClearAllPoints(); hNoLbl:SetPoint("LEFT", rightCell, "LEFT", colHDD, 0)
            mLbl:ClearAllPoints(); mLbl:SetPoint("LEFT", rightCell, "LEFT", colMCB - 14, 0)
            mCB:ClearAllPoints();  mCB:SetPoint( "LEFT", rightCell, "LEFT", colMCB, 0)
            mDD:ClearAllPoints();  mDD:SetPoint( "LEFT", rightCell, "LEFT", colMDD, 0)
            mNoLbl:ClearAllPoints(); mNoLbl:SetPoint("LEFT", rightCell, "LEFT", colMDD, 0)

            r._hDefaultCD = getDefaultCountdown(ability, "H")
            r._mDefaultCD = getDefaultCountdown(ability, "M")
            r._hOver = CCS.GetCountdownOverride(ability.key, "H")
            r._mOver = CCS.GetCountdownOverride(ability.key, "M")
            r._hCtrl.abilityKey = ability.key
            r._mCtrl.abilityKey = ability.key

            hCB:SetChecked(CCS.IsCDEnabled(ability.key, "H"))
            hDD:SetItems(buildCountdownItems(r._hDefaultCD))
            hDD._defaultSound = r._hDefaultCD
            hDD:SetValue(r._hOver or "__default__")
            mCB:SetChecked(CCS.IsCDEnabled(ability.key, "M"))
            mDD:SetItems(buildCountdownItems(r._mDefaultCD))
            mDD._defaultSound = r._mDefaultCD
            mDD:SetValue(r._mOver or "__default__")

            hLbl:Show(); hCB:Show(); hDD:Show(); hNoLbl:Show()
            mLbl:Show(); mCB:Show(); mDD:Show(); mNoLbl:Show()
            refreshHDD(); refreshMDD()
        end
    end

    -- syncFromDB: re-reads DB values into controls without repositioning anything
    function r.syncFromDB()
        local a = r._ability; if not a then return end
        local warnEn = CCS.isWarnEnabled(a.key)
        warnCB:SetChecked(warnEn)
        refreshWarnDD()
        if r._isMplus then
            cdCB:SetChecked(CCS.IsCDEnabled(a.key, "M"))
            refreshCdDD()
        else
            hCB:SetChecked(CCS.IsCDEnabled(a.key, "H"))
            mCB:SetChecked(CCS.IsCDEnabled(a.key, "M"))
            refreshHDD(); refreshMDD()
        end
    end

    -- Permanent ctrl tables for bulk enable/disable — abilityKey stamped in rebind
    r._warnCtrl = {
        cb=warnCB, abilityKey=nil,
        syncFromDB = function() r.syncFromDB() end,
        setEnabled = function(en)
            local a = r._ability; if not a then return end
            CCS.SetWarnEnabled(a.key, en); warnCB:SetChecked(en)
            CCS.RefreshAbility(a.key, a); refreshWarnDD()
        end,
    }
    r._cdCtrl = {
        cb=cdCB, diff="M", abilityKey=nil,
        syncFromDB = function() r.syncFromDB() end,
        setEnabled = function(en)
            local a = r._ability; if not a then return end
            CCS.SetCDEnabled(a.key, "M", en); cdCB:SetChecked(en)
            CCS.RefreshAbility(a.key, a); refreshCdDD()
        end,
    }
    r._hCtrl = {
        cb=hCB, diff="H", abilityKey=nil,
        syncFromDB = function() r.syncFromDB() end,
        setEnabled = function(en)
            local a = r._ability; if not a then return end
            CCS.SetCDEnabled(a.key, "H", en); hCB:SetChecked(en)
            CCS.RefreshAbility(a.key, a); refreshHDD()
        end,
    }
    r._mCtrl = {
        cb=mCB, diff="M", abilityKey=nil,
        syncFromDB = function() r.syncFromDB() end,
        setEnabled = function(en)
            local a = r._ability; if not a then return end
            CCS.SetCDEnabled(a.key, "M", en); mCB:SetChecked(en)
            CCS.RefreshAbility(a.key, a); refreshMDD()
        end,
    }

    _pool.rows[idx] = r
    return r
end

local function acquireHeader(scrollChild, idx)
    if not _pool.headers[idx] then
        _pool.headers[idx] = scrollChild:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge")
    end
    return _pool.headers[idx]
end

local function acquireRaidBg(scrollChild, idx)
    if not _pool.raidBgs[idx] then
        local bg  = CreateFrame("Frame", nil, scrollChild)
        local tex = bg:CreateTexture(nil, "BACKGROUND")
        tex:SetAllPoints(); tex:SetColorTexture(0.078, 0.078, 0.078, 1)
        local lbl = bg:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        lbl:SetPoint("CENTER"); lbl:SetJustifyH("CENTER")
        bg._lbl = lbl
        _pool.raidBgs[idx] = bg
    end
    return _pool.raidBgs[idx]
end

local function acquireSep(scrollChild, idx)
    if not _pool.seps[idx] then
        local s = scrollChild:CreateTexture(nil, "BACKGROUND")
        s:SetHeight(1)
        _pool.seps[idx] = s
    end
    return _pool.seps[idx]
end

local function acquireDivider(scrollChild)
    if not _pool.divider then
        local d = scrollChild:CreateTexture(nil, "BACKGROUND")
        d:SetColorTexture(0.3, 0.3, 0.3, 0.8)
        d:SetWidth(1)
        _pool.divider = d
    end
    return _pool.divider
end

------------------------------------------------------------
-- One-time frame creation (called once on first panel show)
------------------------------------------------------------

local function BuildFramePool(scrollChild)
    local rowIdx  = 0
    local hdrCount, raidCount, sepCount = 0, 0, 0
    for _, entry in ipairs(CCS_Spells) do
        if entry.abilities then
            hdrCount = hdrCount + 1
            if entry.raid then raidCount = raidCount + 1; sepCount = sepCount + 2 end
            sepCount = sepCount + 1
            for _ in ipairs(entry.abilities) do
                rowIdx = rowIdx + 1
                acquireRow(scrollChild, rowIdx)
            end
        end
    end
    for i = 1, hdrCount  do acquireHeader(scrollChild,  i) end
    for i = 1, raidCount do acquireRaidBg(scrollChild,  i) end
    for i = 1, sepCount  do acquireSep(scrollChild,     i) end
    acquireDivider(scrollChild)
end

------------------------------------------------------------
-- rebindAll: zero allocations — called on every module/dungeon switch
------------------------------------------------------------

local function rebindAll(scrollChild, totalWidth, leftW, isMplus,
                         allWarnCBs, allWarnDDs, defaultWarnCBs, noDefaultWarnCBs,
                         defaultUCBs, noDefaultUCBs)
    local rightW = totalWidth - leftW

    wipe(allWarnCBs); wipe(allWarnDDs)
    wipe(defaultWarnCBs); wipe(noDefaultWarnCBs)
    wipe(defaultUCBs); wipe(noDefaultUCBs)

    local y        = -8
    local rowIdx   = 0
    local hdrIdx   = 0
    local raidIdx  = 0
    local sepIdx   = 0
    local lastRaid = nil
    local divTopY  = nil

    local divider = _pool.divider

    for _, entry in ipairs(CCS_Spells) do
        if entry.abilities then

        local justRaidHdr = false

        if entry.raid and entry.raid ~= lastRaid then
            lastRaid     = entry.raid
            justRaidHdr  = true
            if y < -8 then y = y - 8 end

            sepIdx = sepIdx + 1
            local s1 = acquireSep(scrollChild, sepIdx)
            s1:SetColorTexture(0.4, 0.4, 0.4, 0.5)
            s1:ClearAllPoints()
            s1:SetPoint("TOPLEFT",  scrollChild, "TOPLEFT", 0,          y)
            s1:SetPoint("TOPRIGHT", scrollChild, "TOPLEFT", totalWidth, y)
            s1:Show(); y = y - 1

            raidIdx = raidIdx + 1
            local bg = acquireRaidBg(scrollChild, raidIdx)
            bg:ClearAllPoints()
            bg:SetPoint("TOPLEFT",  scrollChild, "TOPLEFT", 0,          y)
            bg:SetPoint("TOPRIGHT", scrollChild, "TOPLEFT", totalWidth, y)
            bg:SetHeight(32)
            bg._lbl:SetText((RAID_COLORS[entry.raid] or "|cffcccccc") .. entry.raid .. "|r")
            bg:Show(); y = y - 32

            sepIdx = sepIdx + 1
            local s2 = acquireSep(scrollChild, sepIdx)
            s2:SetColorTexture(0.4, 0.4, 0.4, 0.5)
            s2:ClearAllPoints()
            s2:SetPoint("TOPLEFT",  scrollChild, "TOPLEFT", 0,          y)
            s2:SetPoint("TOPRIGHT", scrollChild, "TOPLEFT", totalWidth, y)
            s2:Show()
            if not divTopY then divTopY = y - 1 end
            y = y - 8
        end

        if not justRaidHdr and y < -8 then
            sepIdx = sepIdx + 1
            local sep = acquireSep(scrollChild, sepIdx)
            sep:SetColorTexture(0.25, 0.25, 0.25, 0.6)
            sep:ClearAllPoints()
            sep:SetPoint("TOPLEFT",  scrollChild, "TOPLEFT", 0,          y)
            sep:SetPoint("TOPRIGHT", scrollChild, "TOPLEFT", totalWidth, y)
            sep:Show(); y = y - 6
        end

        hdrIdx = hdrIdx + 1
        local hdr = acquireHeader(scrollChild, hdrIdx)
        hdr:ClearAllPoints()
        hdr:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", INDENT, y)
        hdr:SetWidth(totalWidth - INDENT * 2)
        hdr:SetJustifyH("LEFT")
        hdr:SetText(entry.section or entry.boss)
        hdr:Show(); y = y - HEADER_H

        for _, ability in ipairs(entry.abilities) do
            rowIdx = rowIdx + 1
            local r = acquireRow(scrollChild, rowIdx)

            r.leftCell:SetSize(leftW, ROW_HEIGHT)
            r.leftCell:ClearAllPoints()
            r.leftCell:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0,     y)
            r.rightCell:SetSize(rightW, ROW_HEIGHT)
            r.rightCell:ClearAllPoints()
            r.rightCell:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", leftW, y)
            r.rebind(ability, isMplus)
            r.leftCell:Show(); r.rightCell:Show()

            allWarnCBs[#allWarnCBs + 1] = r.warnCB
            allWarnDDs[#allWarnDDs + 1] = r.warnDD
            local wt = r._warnNoDefault and noDefaultWarnCBs or defaultWarnCBs
            wt[#wt + 1] = r._warnCtrl

            if isMplus then
                local t = r._cdDefaultCD and defaultUCBs or noDefaultUCBs
                t[#t + 1] = r._cdCtrl
            else
                if hasHeroic(ability) then
                    local t = r._hDefaultCD and defaultUCBs or noDefaultUCBs
                    t[#t + 1] = r._hCtrl
                end
                if hasMythic(ability) then
                    local t = r._mDefaultCD and defaultUCBs or noDefaultUCBs
                    t[#t + 1] = r._mCtrl
                end
            end

            y = y - ROW_HEIGHT - 1
        end
        y = y - 8

        end  -- if entry.abilities
    end

    -- Hide unused pool entries
    for i = rowIdx + 1, #_pool.rows   do _pool.rows[i].leftCell:Hide(); _pool.rows[i].rightCell:Hide() end
    for i = hdrIdx + 1, #_pool.headers do _pool.headers[i]:Hide() end
    for i = raidIdx + 1, #_pool.raidBgs do _pool.raidBgs[i]:Hide() end
    for i = sepIdx  + 1, #_pool.seps   do _pool.seps[i]:Hide() end

    local contentH = math.abs(y) + 16
    scrollChild:SetHeight(contentH)

    local topY = divTopY or 0
    divider:ClearAllPoints()
    divider:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", leftW, topY)
    local visibleH = scrollChild:GetParent() and scrollChild:GetParent():GetHeight() or 0
    divider:SetHeight(math.max(contentH, visibleH) - math.abs(topY))
    divider:Show()
end

------------------------------------------------------------
-- Options Panel
------------------------------------------------------------

local function BuildCCSOptions(panel, isStandalone)
    -- Forward declarations
    local built, scrollChild, fullRebuild

    -- Top block
    local topBlock = CreateFrame("Frame", nil, panel)
    topBlock:SetPoint("TOPLEFT",  panel, "TOPLEFT",   6, -6)
    topBlock:SetPoint("TOPRIGHT", panel, "TOPRIGHT", isStandalone and -6 or -36, -6)
    topBlock:SetHeight(96)
    local topBlockBg = topBlock:CreateTexture(nil, "BACKGROUND")
    topBlockBg:SetAllPoints()
    topBlockBg:SetColorTexture(0.078, 0.078, 0.078, 1)

    local title = topBlock:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 16, -12)
    title:SetText(CATEGORY_NAME)

    local loadDefaultsBtn = CreateFrame("Button", nil, topBlock, "UIPanelButtonTemplate")
    loadDefaultsBtn:SetSize(70, 20)
    loadDefaultsBtn:SetPoint("RIGHT", topBlock, "TOPRIGHT", -42, -14)
    loadDefaultsBtn:SetText("Defaults")
    stripButtonBorder(loadDefaultsBtn)
    addTooltip(loadDefaultsBtn, "Load Default Configuration",
        "Resets all settings on your current profile back to the default settings.")

    local profilesBtn = CreateFrame("Button", nil, topBlock, "UIPanelButtonTemplate")
    profilesBtn:SetSize(60, 20)
    profilesBtn:SetPoint("RIGHT", loadDefaultsBtn, "LEFT", -6, 0)
    profilesBtn:SetText("Profiles")
    stripButtonBorder(profilesBtn)
    addTooltip(profilesBtn, "Profiles", "Open the profile manager.")

    profilesBtn:SetScript("OnClick", function()
        local AceConfigDialog = LibStub("AceConfigDialog-3.0")
        local AceConfig       = LibStub("AceConfig-3.0")
        local AceDBOptions    = LibStub("AceDBOptions-3.0")
        if not CCS._profileOptionsRegistered then
            AceConfig:RegisterOptionsTable("CCSProfiles", AceDBOptions:GetOptionsTable(CCS.GetDB()))
            CCS._profileOptionsRegistered = true
        end
        if AceConfigDialog.OpenFrames["CCSProfiles"] then
            AceConfigDialog:Close("CCSProfiles")
        else
            AceConfigDialog:Open("CCSProfiles")
        end
    end)

    local inst = topBlock:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
    inst:SetPoint("TOPLEFT",  title,    "BOTTOMLEFT", 0,    -6)
    inst:SetPoint("TOPRIGHT", topBlock, "TOPRIGHT",  -16,    0)
    inst:SetJustifyH("LEFT"); inst:SetWordWrap(true)
    inst:SetText(
        "Choose sounds that play automatically when a private aura lands on you in Heroic or Mythic raid.\n" ..
        "Warning sounds (left side) and countdown sounds (right side) are toggled independently."
    )

    -- Confirm dialog (for Defaults button)
    local confirmDialog = CreateFrame("Frame", nil, topBlock, "BackdropTemplate")
    confirmDialog:SetSize(260, 70)
    confirmDialog:SetPoint("TOPRIGHT", topBlock, "TOPRIGHT", -12, -36)
    confirmDialog:SetFrameStrata("FULLSCREEN")
    confirmDialog:SetBackdrop({
        bgFile   = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 10, insets = { left=3, right=3, top=3, bottom=3 },
    })
    confirmDialog:SetBackdropColor(0.08, 0.08, 0.08, 1)
    confirmDialog:SetBackdropBorderColor(0.8, 0.6, 0.1, 1)
    confirmDialog:Hide()

    local confirmText = confirmDialog:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    confirmText:SetPoint("TOP", confirmDialog, "TOP", 0, -10)
    confirmText:SetText("|cffffff00This will remove your custom settings\nand load the built-in defaults.|r")
    confirmText:SetJustifyH("CENTER")

    local yesBtn = CreateFrame("Button", nil, confirmDialog, "UIPanelButtonTemplate")
    yesBtn:SetSize(60, 20)
    yesBtn:SetPoint("BOTTOMLEFT", confirmDialog, "BOTTOMLEFT", 14, 8)
    yesBtn:SetText("Yes")

    local noBtn = CreateFrame("Button", nil, confirmDialog, "UIPanelButtonTemplate")
    noBtn:SetSize(60, 20)
    noBtn:SetPoint("BOTTOMRIGHT", confirmDialog, "BOTTOMRIGHT", -14, 8)
    noBtn:SetText("No")
    noBtn:SetScript("OnClick", function() confirmDialog:Hide() end)

    loadDefaultsBtn:SetScript("OnClick", function()
        if confirmDialog:IsShown() then confirmDialog:Hide() else confirmDialog:Show() end
    end)

    -- Bulk action group boxes
    local function makeGroupBox(parent, anchor, anchorPt, xOff, yOff)
        local box = CreateFrame("Frame", nil, parent, "BackdropTemplate")
        box:SetBackdrop({ bgFile="Interface\\Buttons\\WHITE8x8", edgeFile="Interface\\Buttons\\WHITE8x8", edgeSize=1 })
        box:SetBackdropColor(0, 0, 0, 0)
        box:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.8)
        box:SetPoint("TOPLEFT", anchor, anchorPt, xOff, yOff)
        return box
    end

    local function makeGroupButtons(box, labelText, enableTip, disableTip)
        local lbl = box:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
        lbl:SetText(labelText)
        lbl:SetPoint("TOPLEFT", box, "TOPLEFT", 14, -7)
        local enBtn = CreateFrame("Button", nil, box, "UIPanelButtonTemplate")
        enBtn:SetSize(60, 20); enBtn:SetPoint("LEFT", lbl, "RIGHT", 8, 0)
        enBtn:SetText("Enable"); stripButtonBorder(enBtn); addTooltip(enBtn, "Enable", enableTip)
        local disBtn = CreateFrame("Button", nil, box, "UIPanelButtonTemplate")
        disBtn:SetSize(60, 20); disBtn:SetPoint("LEFT", enBtn, "RIGHT", 2, 0)
        disBtn:SetText("Disable"); stripButtonBorder(disBtn); addTooltip(disBtn, "Disable", disableTip)
        return enBtn, disBtn
    end

    local warnBox = makeGroupBox(topBlock, inst, "BOTTOMLEFT", 0, -8)
    warnBox:SetSize(245, 24)
    local enableWarnBtn, disableWarnBtn = makeGroupButtons(warnBox,
        "|cffcccccc All Spell Warnings|r",
        "Turn on the default or user-set warning sounds for every ability.",
        "Turn off all warning sounds for every ability.")

    local cdBox = makeGroupBox(topBlock, warnBox, "TOPRIGHT", 45, 0)
    cdBox:SetSize(230, 24)
    local enableCDBtn, disableCDBtn = makeGroupButtons(cdBox,
        "|cffcccccc All Countdowns|r",
        "Turn on the default or user-set countdown sounds for every ability.",
        "Turn off all countdown sounds.")

    local moduleBox = makeGroupBox(topBlock, cdBox, "TOPRIGHT", 45, 0)
    moduleBox:SetSize(180, 24)
    local moduleLbl = moduleBox:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
    moduleLbl:SetText("|cffcccccc Module|r")
    moduleLbl:SetPoint("TOPLEFT", moduleBox, "TOPLEFT", 14, -7)
    local raidBtn = CreateFrame("Button", nil, moduleBox, "UIPanelButtonTemplate")
    raidBtn:SetSize(50, 20); raidBtn:SetPoint("LEFT", moduleLbl, "RIGHT", 8, 0)
    raidBtn:SetText("Raid"); stripButtonBorder(raidBtn)
    addTooltip(raidBtn, "Raid Module", "Switch to raid spell data.")
    local mplusBtn = CreateFrame("Button", nil, moduleBox, "UIPanelButtonTemplate")
    mplusBtn:SetSize(60, 20); mplusBtn:SetPoint("LEFT", raidBtn, "RIGHT", 2, 0)
    mplusBtn:SetText("Mythic+"); stripButtonBorder(mplusBtn)
    addTooltip(mplusBtn, "Mythic+ Module", "Switch to Mythic+ spell data.")
    raidBtn:SetScript("OnClick",  function() CCS.SetModule("raid")  end)
    mplusBtn:SetScript("OnClick", function() CCS.SetModule("mplus") end)
    moduleBox:SetShown(CCS.MPLUS_ENABLED)

    -- Divider below top block
    local topDivider = panel:CreateTexture(nil, "ARTWORK")
    topDivider:SetColorTexture(0.35, 0.35, 0.35, 0.8)
    topDivider:SetHeight(1)
    topDivider:SetPoint("TOPLEFT",  topBlock, "BOTTOMLEFT",  0, 0)
    topDivider:SetPoint("TOPRIGHT", topBlock, "BOTTOMRIGHT", 0, 0)

    -- Column header bar
    local headerBar = CreateFrame("Frame", nil, panel)
    headerBar:SetHeight(42)
    headerBar:SetPoint("TOPLEFT",  topBlock, "BOTTOMLEFT",  0, -6)
    headerBar:SetPoint("TOPRIGHT", topBlock, "BOTTOMRIGHT", 0, -6)

    local leftHdrTitle = headerBar:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    leftHdrTitle:SetText("|cffFFD100Warning Sound|r")
    local rightHdrTitle = headerBar:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    rightHdrTitle:SetText("|cffFFD100Countdown Timer|r")
    local rightHdrSub = headerBar:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
    rightHdrSub:SetText("|cffaaaaaa HC: Heroic          M: Mythic|r")

    -- Dungeon tab strip (M+ only)
    local TAB_H   = 28
    local TAB_PAD = 8
    local TAB_GAP = 2
    local tabStrip = CreateFrame("Frame", nil, panel, "BackdropTemplate")
    tabStrip:SetSize(110, TAB_PAD + ((#CCS_MplusDungeons + 1) * (TAB_H + TAB_GAP)) - TAB_GAP + TAB_PAD)
    tabStrip:SetPoint("TOPLEFT", headerBar, "TOPRIGHT", 4, 0)
    tabStrip:SetBackdrop({ edgeFile="Interface\\Buttons\\WHITE8x8", edgeSize=1 })
    tabStrip:SetBackdropBorderColor(0.35, 0.35, 0.35, 0.8)
    local tabBg = tabStrip:CreateTexture(nil, "BACKGROUND")
    tabBg:SetAllPoints(); tabBg:SetColorTexture(0.078, 0.078, 0.078, 1)

    local tabButtons = {}

    -- "All" tab at the top
    local allTab = CreateFrame("Button", nil, tabStrip, "UIPanelButtonTemplate")
    allTab:SetHeight(TAB_H)
    allTab:SetPoint("TOPLEFT",  tabStrip, "TOPLEFT",  4,  -TAB_PAD)
    allTab:SetPoint("TOPRIGHT", tabStrip, "TOPRIGHT", -4, -TAB_PAD)
    allTab:SetText("All")
    stripButtonBorder(allTab)
    local allFs = allTab:GetFontString()
    if allFs then
        allFs:SetPoint("LEFT",  allTab, "LEFT",  6, 0)
        allFs:SetPoint("RIGHT", allTab, "RIGHT", -6, 0)
        allFs:SetJustifyH("LEFT"); allFs:SetWordWrap(false); allFs:SetNonSpaceWrap(false)
    end
    allTab:SetScript("OnClick", function() CCS.SetDungeon("__all__") end)
    tabButtons["__all__"] = allTab

    for i, dungeon in ipairs(CCS_MplusDungeons) do
        local tab = CreateFrame("Button", nil, tabStrip, "UIPanelButtonTemplate")
        tab:SetHeight(TAB_H)
        tab:SetPoint("TOPLEFT",  tabStrip, "TOPLEFT",  4,  -i*(TAB_H+TAB_GAP) - TAB_PAD)
        tab:SetPoint("TOPRIGHT", tabStrip, "TOPRIGHT", -4, -i*(TAB_H+TAB_GAP) - TAB_PAD)
        tab:SetText(dungeon.label)
        stripButtonBorder(tab)
        local fs = tab:GetFontString()
        if fs then
            fs:SetPoint("LEFT",  tab, "LEFT",  6, 0)
            fs:SetPoint("RIGHT", tab, "RIGHT", -6, 0)
            fs:SetJustifyH("LEFT"); fs:SetWordWrap(false); fs:SetNonSpaceWrap(false)
        end
        tab:SetScript("OnClick", function() CCS.SetDungeon(dungeon.key) end)
        tabButtons[dungeon.key] = tab
    end

    local function refreshTabs()
        local active = CCS.GetActiveDungeon()
        for key, tab in pairs(tabButtons) do
            local fs = tab:GetFontString()
            if fs then fs:SetTextColor(key == active and 1 or 0.7, key == active and 0.82 or 0.7, key == active and 0 or 0.7, 1) end
        end
    end

    local function syncTabVisibility()
        tabStrip:SetShown(CCS.MPLUS_ENABLED and CCS.GetModule() == "mplus")
        refreshTabs()
    end
    syncTabVisibility()

    -- Scroll frame
    local scroll = CreateFrame("ScrollFrame", nil, panel, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT",     headerBar, "BOTTOMLEFT",  0,  -2)
    scroll:SetPoint("BOTTOMRIGHT", panel,     "BOTTOMRIGHT", -6,  10)

    if scroll.ScrollBar then
        scroll.ScrollBar:ClearAllPoints()
        scroll.ScrollBar:SetPoint("TOPRIGHT",    scroll, "TOPRIGHT",    0, -16)
        scroll.ScrollBar:SetPoint("BOTTOMRIGHT", scroll, "BOTTOMRIGHT", 0,  16)
    end

    local function updateScrollBar()
        if not scroll.ScrollBar then return end
        local contentH = scrollChild and scrollChild:GetHeight() or 0
        local viewH    = scroll:GetHeight()
        local range    = math.max(0, contentH - viewH)
        local needs    = range > 0
        scroll.ScrollBar:SetShown(needs)
        if needs then
            scroll.ScrollBar:SetMinMaxValues(0, range)
            scroll.ScrollBar:SetValueStep(ROW_HEIGHT)
            scroll.ScrollBar:SetValue(math.min(scroll:GetVerticalScroll(), range))
        else
            scroll:SetVerticalScroll(0)
        end
    end

    scroll:EnableMouseWheel(true)
    scroll:SetScript("OnMouseWheel", function(self, delta)
        local max = math.max(0, scrollChild:GetHeight() - self:GetHeight())
        self:SetVerticalScroll(math.max(0, math.min(self:GetVerticalScroll() - delta * ROW_HEIGHT * 3, max)))
        if scroll.ScrollBar then scroll.ScrollBar:SetValue(self:GetVerticalScroll()) end
    end)

    if scroll.ScrollBar then
        scroll.ScrollBar:SetScript("OnValueChanged", function(self, value)
            scroll:SetVerticalScroll(value)
        end)
    end
    scroll:HookScript("OnShow",        updateScrollBar)
    scroll:HookScript("OnSizeChanged", updateScrollBar)

    scrollChild = CreateFrame("Frame")
    scrollChild:SetWidth(1); scrollChild:SetHeight(1)
    scroll:SetScrollChild(scrollChild)

    -- Shared state populated by rebindAll, read by bulk buttons
    local _allWarnCBs      = {}
    local _allWarnDDs      = {}
    local _defaultWarnCBs  = {}   -- warn ctrls with a default sound
    local _noDefaultWarnCBs = {}  -- warn ctrls with no default sound
    local _defaultUCBs     = {}   -- CD ctrls with a default
    local _noDefaultUCBs   = {}   -- CD ctrls with no default

    -- Helper: reposition column headers based on current module
    local function updateHeaders(leftW, cdCx)
        local warnDDCx = leftW - 8 - DD_W / 2
        leftHdrTitle:ClearAllPoints()
        leftHdrTitle:SetPoint("CENTER", headerBar, "LEFT", warnDDCx - 20, 0)
        rightHdrSub:ClearAllPoints()
        rightHdrTitle:ClearAllPoints()
        if CCS.GetModule() == "mplus" then
            rightHdrSub:Hide()
            rightHdrTitle:SetPoint("CENTER", headerBar, "LEFT", leftW + cdCx, 1)
        else
            rightHdrSub:Show()
            rightHdrSub:SetPoint("CENTER",  headerBar, "LEFT", leftW + cdCx, -14)
            rightHdrTitle:SetPoint("BOTTOM", rightHdrSub, "TOP", 0, 2)
        end
    end

    local _leftW = nil  -- set on first build, stable thereafter

    fullRebuild = function()
        if not built or not _leftW then return end
        local w = scroll:GetWidth()
        local cdCx = ((colHCB - 14) + (colMDD + CD_W)) / 2
        moduleBox:SetShown(CCS.MPLUS_ENABLED)
        rebindAll(scrollChild, w, _leftW, CCS.GetModule() == "mplus",
                  _allWarnCBs, _allWarnDDs, _defaultWarnCBs, _noDefaultWarnCBs,
                  _defaultUCBs, _noDefaultUCBs)
        updateHeaders(_leftW, cdCx)
        syncTabVisibility()
        updateScrollBar()
    end

    -- Profile change hook — sync values in-place if built, else OnShow handles it
    local _prevOnProfileChange = CCS._onProfileChange
    CCS._onProfileChange = function()
        if _prevOnProfileChange then _prevOnProfileChange() end
        if built then
            for _, r in ipairs(_pool.rows) do r.syncFromDB() end
        end
    end

    -- Module/dungeon change hook
    CCS._fullRebuild = function() fullRebuild() end

    -- Bulk buttons
    enableWarnBtn:SetScript("OnClick", function()
        withCombatGuard(function()
            CCS.SetAllWarn(true)
            for _, r in ipairs(_pool.rows) do r.syncFromDB() end
            CCS.RefreshSounds()
        end)
    end)
    disableWarnBtn:SetScript("OnClick", function()
        withCombatGuard(function()
            CCS.SetAllWarn(false)
            for _, r in ipairs(_pool.rows) do r.syncFromDB() end
            CCS.RefreshSounds()
        end)
    end)
    enableCDBtn:SetScript("OnClick", function()
        withCombatGuard(function()
            CCS.SetAllCD(true)
            for _, r in ipairs(_pool.rows) do r.syncFromDB() end
            CCS.RefreshSounds()
        end)
    end)
    disableCDBtn:SetScript("OnClick", function()
        withCombatGuard(function()
            CCS.SetAllCD(false)
            for _, r in ipairs(_pool.rows) do r.syncFromDB() end
            CCS.RefreshSounds()
        end)
    end)

    -- Yes button wired once on first build
    yesBtn:SetScript("OnClick", function()
        confirmDialog:Hide()
        withCombatGuard(function()
            CCS.ResetProfile()
            fullRebuild()
        end)
    end)

    panel:SetScript("OnShow", function()
        -- Invalidate LSM/warn item cache so newly installed sounds appear
        cachedSoundItems = nil
        wipe(_warnItemsCache)

        if not built then
            built = true
            local w = scroll:GetWidth()
            scrollChild:SetWidth(w)
            _leftW = math.floor(w * LEFT_FRAC)

            BuildFramePool(scrollChild)

            local cdCx = ((colHCB - 14) + (colMDD + CD_W)) / 2
            rebindAll(scrollChild, w, _leftW, CCS.GetModule() == "mplus",
                      _allWarnCBs, _allWarnDDs, _defaultWarnCBs, _noDefaultWarnCBs,
                      _defaultUCBs, _noDefaultUCBs)
            updateHeaders(_leftW, cdCx)
            updateScrollBar()
        else
            -- Already built — re-apply module first (handles mplus being disabled
            -- mid-session), then resync layout
            if CCS.ApplyModule then CCS.ApplyModule() end
            fullRebuild()
        end
    end)

    return panel
end

------------------------------------------------------------
-- Minimap Button
------------------------------------------------------------

local toggleStandalone  -- forward declaration

local function CreateMinimapButton()
    local button = CreateFrame("Button", "CCSMinimapButton", Minimap)
    button:SetSize(28, 28)
    button:SetFrameStrata("MEDIUM")
    button:SetFrameLevel(8)

    local icon = button:CreateTexture(nil, "BACKGROUND")
    icon:SetSize(22, 22); icon:SetPoint("CENTER")
    icon:SetTexture("Interface\\AddOns\\" .. addonName .. "\\icon.tga")

    local mask = button:CreateMaskTexture()
    mask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
    mask:SetAllPoints(icon); icon:AddMaskTexture(mask)

    local border = button:CreateTexture(nil, "OVERLAY")
    border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
    border:SetSize(56, 56); border:SetPoint("CENTER", button, "CENTER", 11, -11)

    button:RegisterForDrag("LeftButton")
    button:SetMovable(true)

    local function updatePosition()
        local angle  = math.rad((CCS.GetChar() and CCS.GetChar().minimapAngle) or 220)
        local radius = (Minimap:GetWidth() / 2) + (button:GetWidth() / 2) - 2
        button:ClearAllPoints()
        button:SetPoint("CENTER", Minimap, "CENTER", radius * math.cos(angle), radius * math.sin(angle))
    end

    button:SetScript("OnDragStart", function(self)
        self:SetScript("OnUpdate", function()
            local mx, my = Minimap:GetCenter()
            local cx, cy = GetCursorPosition()
            local scale  = Minimap:GetEffectiveScale()
            CCS.GetChar().minimapAngle = math.deg(math.atan2(cy/scale - my, cx/scale - mx))
            updatePosition()
        end)
    end)
    button:SetScript("OnDragStop", function(self) self:SetScript("OnUpdate", nil) end)

    button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    button:SetScript("OnClick", function(self, btn)
        if InCombatLockdown() then
            print("|cffffff00CCS:|r Cannot open settings during combat.")
            return
        end
        if btn == "RightButton" and IsShiftKeyDown() then
            CCS.GetChar().minimapHidden = true
            button:Hide()
            print("|cffffff00CCS:|r Minimap button hidden. Use |cffffffff/ccs minimap|r to restore.")
            return
        end
        toggleStandalone()
    end)

    addTooltip(button, "Custom Countdown Sounds",
        "Click to open settings.\nDrag to reposition.\nShift+Right-click to hide.")
    updatePosition()
    return button
end

------------------------------------------------------------
-- Register, slash, init
------------------------------------------------------------

local function CreateStubPanel()
    local panel = CreateFrame("Frame", nil, UIParent)
    panel.name  = CATEGORY_NAME

    local title = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", panel, "TOPLEFT", 16, -16)
    title:SetText(CATEGORY_NAME)

    local desc = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
    desc:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -10)
    desc:SetWidth(500); desc:SetJustifyH("LEFT"); desc:SetWordWrap(true)
    desc:SetText(
        "Custom Countdown Sounds lets you configure sounds that play automatically when private auras " ..
        "land on you during Heroic and Mythic raid encounters. Each ability can have an independent " ..
        "warning sound (plays on aura apply) and a countdown sound (plays at a set interval before " ..
        "the ability fires). Sounds can be customised per-ability and saved into profiles."
    )

    local slashTitle = panel:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    slashTitle:SetPoint("TOPLEFT", desc, "BOTTOMLEFT", 0, -20)
    slashTitle:SetText("Slash Commands")

    local prev = slashTitle
    for _, entry in ipairs({
        { cmd="/ccs",         desc="Open the options window."  },
        { cmd="/ccs minimap", desc="Toggle the minimap button." },
    }) do
        local line = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
        line:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, prev == slashTitle and -8 or -4)
        line:SetJustifyH("LEFT")
        line:SetText("|cffffffff" .. entry.cmd .. "|r  —  " .. entry.desc)
        prev = line
    end

    local btn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate")
    btn:SetSize(120, 24); btn:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -20)
    btn:SetText("Open Options")
    btn:SetScript("OnClick", function() toggleStandalone() end)

    return panel
end

local function CreateStandaloneWindow()
    local MIN_H = 300
    local MAX_H = 1200

    local win = CreateFrame("Frame", "CCSStandaloneWindow", UIParent, "BackdropTemplate")
    win:SetSize(780, 600); win:SetPoint("CENTER")
    win:SetFrameStrata("DIALOG"); win:SetMovable(true); win:EnableMouse(true)
    win:RegisterForDrag("LeftButton")
    win:SetScript("OnDragStart", function(self) self:StartMoving() end)
    win:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local left = self:GetLeft()
        local top  = self:GetTop()
        self:ClearAllPoints()
        self:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", left, top)
    end)
    win:SetBackdrop({
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 10, insets = { left=3, right=3, top=3, bottom=3 },
    })
    win:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
    local bg = win:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(); bg:SetColorTexture(0.1, 0.1, 0.1, 0.98)
    win:Hide()

    local closeBtn = CreateFrame("Button", nil, win, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", win, "TOPRIGHT", -4, -4)
    closeBtn:SetScript("OnClick", function() win:Hide() end)

    -- Resize handle
    local handle = CreateFrame("Frame", nil, win)
    handle:SetHeight(8)
    handle:SetPoint("BOTTOMLEFT",  win, "BOTTOMLEFT",  4, 2)
    handle:SetPoint("BOTTOMRIGHT", win, "BOTTOMRIGHT", -4, 2)
    handle:EnableMouse(true)

    local handleTex = handle:CreateTexture(nil, "BACKGROUND")
    handleTex:SetAllPoints()
    handleTex:SetColorTexture(0.5, 0.5, 0.5, 0.3)

    local grip = handle:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    grip:SetPoint("CENTER")
    grip:SetText("|cff666666· · · · ·|r")

    handle:SetScript("OnEnter", function() handleTex:SetColorTexture(0.8, 0.8, 0.8, 0.4) end)
    handle:SetScript("OnLeave", function() handleTex:SetColorTexture(0.5, 0.5, 0.5, 0.3) end)

    handle:SetScript("OnMouseDown", function(self, btn)
        if btn ~= "LeftButton" then return end
        local left = win:GetLeft()
        local top  = win:GetTop()
        win:ClearAllPoints()
        win:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", left, top)
        handle:SetScript("OnUpdate", function()
            local curY = select(2, GetCursorPosition()) / UIParent:GetEffectiveScale()
            local newH = math.max(MIN_H, math.min(MAX_H, win:GetTop() - curY))
            win:SetHeight(newH)
        end)
    end)
    handle:SetScript("OnMouseUp", function()
        handle:SetScript("OnUpdate", nil)
    end)

    return BuildCCSOptions(win, true)
end

local standaloneWindow

toggleStandalone = function()
    if InCombatLockdown() then
        print("|cffffff00CCS:|r Cannot open settings during combat.")
        return
    end
    if not standaloneWindow then return end
    standaloneWindow:SetShown(not standaloneWindow:IsShown())
end

SLASH_CCS1 = "/ccs"
SlashCmdList["CCS"] = function(msg)
    local arg = msg and msg:match("^%s*(%S+)") or ""
    arg = arg:lower()

    if arg == "minimap" then
        local char = CCS.GetChar()
        char.minimapHidden = not char.minimapHidden
        local btn = CCSMinimapButton
        if btn then btn:SetShown(not char.minimapHidden) end
        print("|cffffff00CCS:|r Minimap button " .. (char.minimapHidden
            and "hidden. Use |cffffffff/ccs minimap|r to restore."
            or  "shown."))
        return
    end

    if arg == "debug"       then CCS.DebugProfile(); return end
    if arg == "sounds"      then CCS.DebugSounds();  return end
    if arg == "debugsounds" then CCS.DebugSounds();  return end

    if arg == "mplus" then
        CCS.MPLUS_ENABLED = not CCS.MPLUS_ENABLED
        if not CCS.MPLUS_ENABLED and CCS.GetModule() == "mplus" then
            CCS.SetModule("raid")
        end
        if CCS.ApplyModule then CCS.ApplyModule() end
        print("|cffffff00CCS:|r Mythic+ module " .. (CCS.MPLUS_ENABLED and "|cff00ff00enabled|r." or "|cffff5555disabled|r."))
        if CCS._fullRebuild then CCS._fullRebuild() end
        return
    end

    if arg == "module" then
        local module = msg:match("^%s*%S+%s+(%S+)")
        if module then
            CCS.SetModule(module:lower())
        else
            print("|cffffff00CCS:|r Active module: |cffffffff" .. CCS.GetModule() ..
                  "|r. Use '/ccs module raid' or '/ccs module mplus'.")
        end
        return
    end

    toggleStandalone()
end

local initFrame = CreateFrame("Frame")
initFrame:RegisterEvent("ADDON_LOADED")
initFrame:SetScript("OnEvent", function(self, _, name)
    if name ~= addonName then return end
    standaloneWindow = CreateStandaloneWindow()
    Settings.RegisterAddOnCategory(Settings.RegisterCanvasLayoutCategory(CreateStubPanel(), CATEGORY_NAME))
    local btn = CreateMinimapButton()
    if CCS.GetChar() and CCS.GetChar().minimapHidden then btn:Hide() end

    -- Register as a LibDataBroker launcher if LDB is available
    local LDB = LibStub and LibStub("LibDataBroker-1.1", true)
    if LDB then
        LDB:NewDataObject(CATEGORY_NAME, {
            type  = "launcher",
            label = CATEGORY_NAME,
            icon  = "Interface\\AddOns\\" .. addonName .. "\\icon.tga",
            OnClick = function(_, btn)
                if btn == "RightButton" then
                    toggleStandalone()
                else
                    toggleStandalone()
                end
            end,
            OnTooltipShow = function(tt)
                tt:AddLine(CATEGORY_NAME, 1, 1, 1)
                tt:AddLine("Click to open settings.", 0.8, 0.8, 0.8)
            end,
        })
    end

    self:UnregisterEvent("ADDON_LOADED")
end)