local ADDON_NAME, ns = ...
ns = ns or {}
local Const = ns.Const
local Util = ns.Util
local Meta = ns.Meta
local Remix = ns.Remix
local Controls = ns.Controls
local getThemeStyle
local getPalette
local applyBackdrop
local STRATA_ORDER = { BACKGROUND = 1, LOW = 2, MEDIUM = 3, HIGH = 4, DIALOG = 5, FULLSCREEN = 6, FULLSCREEN_DIALOG = 7, TOOLTIP = 8 }
local function getHigherStrata(strata)
    local order = STRATA_ORDER[strata or "MEDIUM"] or STRATA_ORDER.MEDIUM
    for k, v in pairs(STRATA_ORDER) do
        if v == order + 1 then return k end
    end
    return "TOOLTIP"
end
getThemeStyle = getThemeStyle or function(themeKey)
    return (THEMES and (THEMES[themeKey] or THEMES.Modern)) or {}
end
applyBackdrop = applyBackdrop or function() end
local ICON = "Interface\\AddOns\\" .. (ADDON_NAME or "AlmostCompletedAchievements") .. "\\Media\\Icon.png"

local Panel = {}
ns.Panel = Panel

local buildMetaPanel
local tooltipForRow
local RESULT_ROW_EXTENT = 52
local getNameIconWithFallback
local rememberIgnoredInfo
local rememberIgnoredInfoGlobal
local applyRowIgnoreCloseStyle
local applyRowIgnoreResetStyle
local applyRowIgnoreTextStyle
local applyRowRestoreExpandStyle
local applyRowIgnoreAllExitStyle

local function registerForEscapeClose(frame)
    local name = frame and frame.GetName and frame:GetName()
    if not (name and UISpecialFrames) then return end
    for _, n in ipairs(UISpecialFrames) do
        if n == name then return end
    end
    table.insert(UISpecialFrames, name)
end

local function ensureAchievementUI(show)
    if not AchievementFrame or not AchievementFrame_IsVisible then
        UIParentLoadAddOn("Blizzard_AchievementUI")
    end
    if show ~= false and AchievementFrame then AchievementFrame:Show() end
end

local function openAchievement(id)
    ensureAchievementUI(true)
    local function select()
        local fn = _G.AchievementFrame_SelectAchievement
        if type(fn) == "function" then fn(id) end
    end
    if C_Timer and C_Timer.After then
        C_Timer.After(0, select)
    else
        select()
    end
end

local function trackAchievement(id)
    if not id then return end
    ensureAchievementUI(false)
    local function toggleContentTracking(trackingID)
        local trackingType = Enum and Enum.ContentTrackingType and Enum.ContentTrackingType.Achievement
        if not (C_ContentTracking and trackingType) then return false end
        local stopType = Enum and Enum.ContentTrackingStopType and Enum.ContentTrackingStopType.Manual
        local isTracking = C_ContentTracking.IsTracking and C_ContentTracking.IsTracking(trackingType, trackingID)
        if isTracking then
            if C_ContentTracking.StopTracking then
                local ok = pcall(C_ContentTracking.StopTracking, trackingType, trackingID, stopType)
                if not ok then pcall(C_ContentTracking.StopTracking, trackingType, trackingID) end
                return true
            end
        else
            local maxTracked = Constants and Constants.ContentTrackingConsts and Constants.ContentTrackingConsts.MaxTrackedAchievements
            if maxTracked and C_ContentTracking.GetTrackedIDs then
                local tracked = C_ContentTracking.GetTrackedIDs(trackingType) or {}
                if #tracked >= maxTracked then
                    if UIErrorsFrame and UIErrorsFrame.AddMessage and ACHIEVEMENT_WATCH_TOO_MANY then
                        UIErrorsFrame:AddMessage(format(ACHIEVEMENT_WATCH_TOO_MANY, maxTracked), 1.0, 0.1, 0.1, 1.0)
                    end
                    return true
                end
            end
            local _, _, _, completed, _, _, _, _, _, _, _, isGuild, wasEarnedByMe = GetAchievementInfo(trackingID)
            if (completed and isGuild) or wasEarnedByMe then
                if UIErrorsFrame and UIErrorsFrame.AddMessage and ERR_ACHIEVEMENT_WATCH_COMPLETED then
                    UIErrorsFrame:AddMessage(ERR_ACHIEVEMENT_WATCH_COMPLETED, 1.0, 0.1, 0.1, 1.0)
                end
                return true
            end
            if C_ContentTracking.StartTracking then
                local ok, trackingError = pcall(C_ContentTracking.StartTracking, trackingType, trackingID)
                if ok and trackingError and ContentTrackingUtil and ContentTrackingUtil.DisplayTrackingError then
                    ContentTrackingUtil.DisplayTrackingError(trackingError)
                elseif not ok then
                    pcall(C_ContentTracking.StartTracking, trackingType, trackingID, true)
                end
                return true
            end
        end
        return false
    end
    if AddTrackedAchievement and RemoveTrackedAchievement and IsTrackedAchievement then
        if IsTrackedAchievement(id) then
            RemoveTrackedAchievement(id)
        else
            AddTrackedAchievement(id)
        end
        return
    end
    if AddTrackedAchievement then
        AddTrackedAchievement(id)
        return
    end
    if toggleContentTracking(id) then return end
    if C_AchievementInfo and C_AchievementInfo.SetSuperTrackedAchievement then
        C_AchievementInfo.SetSuperTrackedAchievement(id)
    end
end
Panel.trackAchievement = trackAchievement

local function initLegacyScrollFrame(scrollFrame, rowExtent)
    if Controls and Controls.initLegacyScrollFrame then
        Controls.initLegacyScrollFrame(scrollFrame, rowExtent)
    elseif scrollFrame then
        scrollFrame:EnableMouseWheel(true)
    end
end

local function ensureLinkPopup()
    if Panel.linkPopup then return Panel.linkPopup end
    local frame = CreateFrame("Frame", "ACAU_LinkPopup", UIParent, "BackdropTemplate")
    local baseStrata = (Panel.frame and Panel.frame.GetFrameStrata and Panel.frame:GetFrameStrata()) or "MEDIUM"
    frame:SetFrameStrata(getHigherStrata(baseStrata))
    frame:SetMovable(true)
    frame:EnableMouse(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetScript("OnDragStart", frame.StartMoving)
    frame:SetScript("OnDragStop", frame.StopMovingOrSizing)
    frame:SetClampedToScreen(true)
    frame:SetHeight(64)
    frame:SetWidth(360)
    frame:SetPoint("CENTER")

    local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    title:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -8)
    title:SetText("Wowhead link:")
    frame.title = title

    local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 8, 8)
    close:SetScript("OnClick", function() frame:Hide() end)
    frame.close = close

    local edit = CreateFrame("EditBox", nil, frame, "InputBoxTemplate")
    edit:SetAutoFocus(false)
    edit:SetPoint("TOPLEFT", frame, "TOPLEFT", 10, -28)
    edit:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -10, 10)
    edit:SetFontObject("GameFontHighlight")
    edit:SetTextInsets(6, 6, 0, 0)
    edit:SetScript("OnEscapePressed", function(self) self:ClearFocus(); frame:Hide() end)
    edit:SetScript("OnEditFocusGained", function(self) self:HighlightText() end)
    edit:SetScript("OnMouseUp", function(self) self:HighlightText() end)
    frame.edit = edit

    local hint = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    hint:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -20, -10)
    hint:SetJustifyH("RIGHT")
    hint:SetText("Ctrl+C")
    frame.copyHint = hint

    registerForEscapeClose(frame)
    Panel.linkPopup = frame
    return frame
end

local function openWowheadLink(id, name)
    if not id then return end
    local link = ("https://www.wowhead.com/achievement=%d"):format(id)
    local frame = ensureLinkPopup()
    local themeKey = (ACAU_Config and ACAU_Config.theme) or Const.DEFAULTS.theme
    applyBackdrop(frame, getThemeStyle(themeKey), themeKey)
    local palette = getPalette(themeKey)
    if palette and palette.textPrimary and frame.edit and frame.edit.SetTextColor then
        frame.edit:SetTextColor(palette.textPrimary[1], palette.textPrimary[2], palette.textPrimary[3])
    end
    if palette and palette.textPrimary and frame.title and frame.title.SetTextColor then
        frame.title:SetTextColor(palette.textPrimary[1], palette.textPrimary[2], palette.textPrimary[3])
    end
    if palette and palette.textSecondary and frame.copyHint and frame.copyHint.SetTextColor then
        frame.copyHint:SetTextColor(palette.textSecondary[1], palette.textSecondary[2], palette.textSecondary[3])
    end
    if frame.title and frame.title.SetText then
        local displayName = name or ("#" .. tostring(id))
        frame.title:SetText(("Wowhead link: %s"):format(displayName))
    end
    frame.edit:SetText(link)
    frame.edit:HighlightText()
    frame.edit:SetFocus()
    local desired = 360
    if frame.edit and frame.edit.GetText then
        local txt = frame.edit:GetText() or ""
        desired = (txt:len() * 8) + 36
    end
    local width = Util and Util.Clamp and Util:Clamp(desired, 220, 820) or desired
    frame:SetWidth(width)
    frame:Show()
end

local function handleAchievementClick(id, fallback, button, name)
    if not id then return end
    if button == "RightButton" then
        openWowheadLink(id, name)
        return
    end
    if (not button or button == "LeftButton") and IsShiftKeyDown and IsShiftKeyDown() then
        trackAchievement(id)
    else
        (fallback or openAchievement)(id)
    end
end

local function makeRow(parent)
    local actionButtonTemplate = (Util and Util.IsRetail and Util:IsRetail()) and "UIResetButtonTemplate" or "UIPanelButtonTemplate"
    local f = CreateFrame("Button", nil, parent, "BackdropTemplate")
    f:SetSize(380, 48)
    f:RegisterForClicks("AnyUp")

    f.bg = f:CreateTexture(nil, "BACKGROUND")
    f.bg:SetAllPoints()
    f.bg:SetColorTexture(0, 0, 0, 0.35)

    f.highlight = f:CreateTexture(nil, "HIGHLIGHT")
    f.highlight:SetAllPoints()
    f.highlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
    f.highlight:SetBlendMode("ADD")
    f.highlight:SetAlpha(0.25)

    f.Icon = f:CreateTexture(nil, "ARTWORK")
    f.Icon:SetSize(36, 36)
    f.Icon:SetPoint("LEFT", f, "LEFT", 6, 0)

    f.Name = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    f.Name:SetPoint("TOPLEFT", f.Icon, "TOPRIGHT", 8, 3)
    f.Name:SetWidth(220)
    f.Name:SetJustifyH("LEFT")

    f.Percent = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    f.Percent:SetPoint("TOPLEFT", f.Name, "BOTTOMLEFT", 0, -9)
    f.Percent:SetWidth(120)
    f.Percent:SetJustifyH("LEFT")

    f.Points = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.Ignore = CreateFrame("Button", nil, f, actionButtonTemplate)
    f.Ignore:SetSize(20, 20)
    f.Ignore:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -6)
    f.Ignore:Show()
    applyRowIgnoreCloseStyle(f.Ignore)

    f.IgnoreAll = CreateFrame("Button", nil, f, actionButtonTemplate)
    f.IgnoreAll:SetSize(20, 20)
    f.IgnoreAll:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -30)
    f.IgnoreAll:Show()
    applyRowIgnoreAllExitStyle(f.IgnoreAll)
    f.IgnoreAll:Hide()

    f.Points = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.Points:SetPoint("RIGHT", f.Ignore, "LEFT", -6, 0)
    f.Points:SetWidth(90)
    f.Points:SetJustifyH("RIGHT")

    f.Reward = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.Reward:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -10, 6)
    f.Reward:SetWidth(250)
    f.Reward:SetJustifyH("RIGHT")

    f.Divider = f:CreateTexture(nil, "BORDER")
    f.Divider:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 4, 0)
    f.Divider:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -4, 0)
    f.Divider:SetHeight(1)
    f.Divider:SetColorTexture(1, 1, 1, 0.08)

    return f
end

applyRowIgnoreCloseStyle = function(btn)
    if not btn then return end
    if btn.Left then btn.Left:SetAlpha(0) end
    if btn.Middle then btn.Middle:SetAlpha(0) end
    if btn.Right then btn.Right:SetAlpha(0) end
    btn:SetText("")
    btn:SetSize(20, 20)
    if btn.SetNormalAtlas and btn.SetPushedAtlas and btn.SetHighlightAtlas then
        btn:SetNormalAtlas("RedButton-Exit")
        btn:SetPushedAtlas("RedButton-exit-pressed")
        btn:SetHighlightAtlas("RedButton-Highlight", "ADD")
        if btn.SetDisabledAtlas then
            btn:SetDisabledAtlas("RedButton-Exit-Disabled")
        end
    else
        btn:SetNormalTexture("Interface\\Buttons\\UI-Panel-HideButton-Up")
        btn:SetPushedTexture("Interface\\Buttons\\UI-Panel-HideButton-Down")
        btn:SetHighlightTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Highlight", "ADD")
    end
end

applyRowIgnoreResetStyle = function(btn)
    if not btn then return end
    if btn.Left then btn.Left:SetAlpha(0) end
    if btn.Middle then btn.Middle:SetAlpha(0) end
    if btn.Right then btn.Right:SetAlpha(0) end
    btn:SetText("")
    btn:SetSize(23, 23)
    if btn.SetNormalAtlas and btn.SetHighlightAtlas then
        btn:SetNormalAtlas("auctionhouse-ui-filter-redx")
        if btn.SetPushedAtlas then
            btn:SetPushedAtlas("auctionhouse-ui-filter-redx")
        end
        btn:SetHighlightAtlas("auctionhouse-ui-filter-redx", "ADD")
        local ht = btn.GetHighlightTexture and btn:GetHighlightTexture()
        if ht and ht.SetAlpha then
            ht:SetAlpha(0.4)
        end
        if btn.SetDisabledAtlas then
            btn:SetDisabledAtlas("auctionhouse-ui-filter-redx")
        end
    else
        applyRowIgnoreCloseStyle(btn)
    end
end

applyRowIgnoreTextStyle = function(btn, text, width, height)
    if not btn then return end
    btn:SetNormalTexture("")
    btn:SetPushedTexture("")
    btn:SetHighlightTexture("")
    if btn.Left then btn.Left:SetAlpha(1) end
    if btn.Middle then btn.Middle:SetAlpha(1) end
    if btn.Right then btn.Right:SetAlpha(1) end
    btn:SetText(text or "")
    if width and height then
        btn:SetSize(width, height)
    end
end

applyRowRestoreExpandStyle = function(btn)
    if not btn then return end
    if btn.Left then btn.Left:SetAlpha(0) end
    if btn.Middle then btn.Middle:SetAlpha(0) end
    if btn.Right then btn.Right:SetAlpha(0) end
    btn:SetText("")
    btn:SetSize(20, 20)
    btn:SetNormalTexture("Interface\\Buttons\\UI-Panel-ExpandButton-Up")
    btn:SetPushedTexture("Interface\\Buttons\\UI-Panel-ExpandButton-Down")
    btn:SetHighlightTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Highlight", "ADD")
end

applyRowIgnoreAllExitStyle = function(btn)
    applyRowIgnoreResetStyle(btn)
end

local function ensureScrollResultRow(row)
    local actionButtonTemplate = (Util and Util.IsRetail and Util:IsRetail()) and "UIResetButtonTemplate" or "UIPanelButtonTemplate"
    if row._acaBuilt then return end
    row._acaBuilt = true
    row:RegisterForClicks("AnyUp")

    row.bg = row:CreateTexture(nil, "BACKGROUND")
    row.bg:SetAllPoints()
    row.bg:SetColorTexture(0, 0, 0, 0.35)

    row.highlight = row:CreateTexture(nil, "HIGHLIGHT")
    row.highlight:SetAllPoints()
    row.highlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
    row.highlight:SetBlendMode("ADD")
    row.highlight:SetAlpha(0.25)

    row.Icon = row:CreateTexture(nil, "ARTWORK")
    row.Icon:SetSize(36, 36)
    row.Icon:SetPoint("LEFT", row, "LEFT", 6, 0)

    row.Name = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row.Name:SetPoint("TOPLEFT", row.Icon, "TOPRIGHT", 8, 3)
    row.Name:SetWidth(220)
    row.Name:SetJustifyH("LEFT")

    row.Percent = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    row.Percent:SetPoint("TOPLEFT", row.Name, "BOTTOMLEFT", 0, -9)
    row.Percent:SetWidth(120)
    row.Percent:SetJustifyH("LEFT")

    row.Ignore = CreateFrame("Button", nil, row, actionButtonTemplate)
    row.Ignore:SetSize(20, 20)
    row.Ignore:SetPoint("TOPRIGHT", row, "TOPRIGHT", -8, -6)
    row.Ignore:Show()
    applyRowIgnoreCloseStyle(row.Ignore)

    row.IgnoreAll = CreateFrame("Button", nil, row, actionButtonTemplate)
    row.IgnoreAll:SetSize(20, 20)
    row.IgnoreAll:SetPoint("TOPRIGHT", row, "TOPRIGHT", -8, -30)
    row.IgnoreAll:Show()
    applyRowIgnoreAllExitStyle(row.IgnoreAll)
    row.IgnoreAll:Hide()

    row.Points = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.Points:SetPoint("RIGHT", row.Ignore, "LEFT", -6, 0)
    row.Points:SetWidth(90)
    row.Points:SetJustifyH("RIGHT")

    row.Reward = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.Reward:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", -10, 6)
    row.Reward:SetWidth(250)
    row.Reward:SetJustifyH("RIGHT")

    row.Divider = row:CreateTexture(nil, "BORDER")
    row.Divider:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 4, 0)
    row.Divider:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", -4, 0)
    row.Divider:SetHeight(1)
    row.Divider:SetColorTexture(1, 1, 1, 0.08)
end

local function ensureIgnoredScrollRow(row)
    ensureScrollResultRow(row)
    applyRowRestoreExpandStyle(row.Ignore)
    row.Ignore:SetPoint("TOPRIGHT", row, "TOPRIGHT", -8, -26)
    applyRowIgnoreAllExitStyle(row.IgnoreAll)
    row.IgnoreAll:SetSize(20, 20)
    row.IgnoreAll:SetPoint("TOPRIGHT", row, "TOPRIGHT", -8, -6)
end

local function createMainActionButton(parent, text, onClick)
    if Controls and Controls.createMainActionButton then
        return Controls.createMainActionButton(parent, text, onClick)
    end
    local btn = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    btn:SetHeight(22)
    btn:SetText(text or "")
    btn:SetScript("OnClick", onClick)
    return btn
end

local function createMainPanelButton(panel, text, onClick)
    return createMainActionButton(panel, text, onClick)
end

local function createMainActionFilterButton(panel)
    return createMainPanelButton(panel, "Filters", function()
        if ns.Panel and ns.Panel.ToggleFilters then
            ns.Panel:ToggleFilters()
        end
    end)
end

local function createCheck(parent, label, initial, onClick)
    local cb = CreateFrame("CheckButton", nil, parent, "InterfaceOptionsCheckButtonTemplate")
    cb.Text:SetText(label)
    cb:SetChecked(initial)
    cb:SetScript("OnClick", function(self) onClick(not not self:GetChecked()) end)
    return cb
end

local function createSlider(parent, label, min, max, step, value, onChanged)
    local holder = CreateFrame("Frame", nil, parent, "MinimalSliderWithSteppersTemplate")
    local slider = holder and holder.Slider
    if holder and slider then
        holder:SetFrameLevel((parent:GetFrameLevel() or 1) + 2)
        slider:EnableMouse(true)
        slider:SetMinMaxValues(min, max)
        slider:SetValueStep(step)
        slider:SetObeyStepOnDrag(true)
        slider:SetValue(value)
        holder._min, holder._max, holder._step = min, max, step
        holder.Text = holder.Text or holder:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        holder.Text:ClearAllPoints()
        holder.Text:SetPoint("BOTTOM", holder, "TOP", 0, 4)
        holder.Text:SetText(("%s: %d%%"):format(label, value))
        if holder.Low and holder.High then
            holder.Low:ClearAllPoints()
            holder.Low:SetPoint("TOPLEFT", holder, "BOTTOMLEFT", 0, -2)
            holder.Low:SetText("Low")
            holder.High:ClearAllPoints()
            holder.High:SetPoint("TOPRIGHT", holder, "BOTTOMRIGHT", 0, -2)
            holder.High:SetText("High")
        end
        slider:SetScript("OnValueChanged", function(self, v)
            local vv = math.floor(v + 0.5)
            if holder.Text then holder.Text:SetText(("%s: %d%%"):format(label, vv)) end
            onChanged(vv)
        end)
        local function nudge(delta)
            local current = slider:GetValue() or value
            local nextVal = math.min(holder._max, math.max(holder._min, current + (delta * holder._step)))
            slider:SetValue(nextVal)
        end
        if holder.DecrementButton then holder.DecrementButton:SetScript("OnClick", function() nudge(-1) end) end
        if holder.IncrementButton then holder.IncrementButton:SetScript("OnClick", function() nudge(1) end) end
        return holder
    end

    local legacy = CreateFrame("Slider", nil, parent, "OptionsSliderTemplate")
    legacy:SetFrameLevel((parent:GetFrameLevel() or 1) + 2)
    legacy:EnableMouse(true)
    legacy:SetMinMaxValues(min, max)
    legacy:SetValueStep(step)
    legacy:SetObeyStepOnDrag(true)
    legacy:SetValue(value)
    legacy._min, legacy._max, legacy._step = min, max, step
    if legacy.SetThumbTexture then
        legacy:SetThumbTexture("Interface\\Buttons\\UI-SliderBar-Button-Horizontal")
    end
    if not legacy.Text then
        legacy.Text = legacy:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    end
    legacy.Text:ClearAllPoints()
    legacy.Text:SetPoint("BOTTOM", legacy, "TOP", 0, 4)
    legacy.Text:SetText(("%s: %d%%"):format(label, value))
    if legacy.Low and legacy.High then
        legacy.Low:ClearAllPoints()
        legacy.Low:SetPoint("TOPLEFT", legacy, "BOTTOMLEFT", 0, -2)
        legacy.Low:SetText("Low")
        legacy.High:ClearAllPoints()
        legacy.High:SetPoint("TOPRIGHT", legacy, "BOTTOMRIGHT", 0, -2)
        legacy.High:SetText("High")
    end
    legacy:SetScript("OnValueChanged", function(self, v)
        local vv = math.floor(v + 0.5)
        if self.Text then self.Text:SetText(("%s: %d%%"):format(label, vv)) end
        onChanged(vv)
    end)

    local function nudge(delta)
        local current = legacy:GetValue() or value
        local nextVal = math.min(legacy._max, math.max(legacy._min, current + (delta * legacy._step)))
        legacy:SetValue(nextVal)
    end
    local left = CreateFrame("Button", nil, legacy, "UIPanelButtonTemplate")
    left:SetSize(18, 18)
    left:SetText("<")
    left:SetPoint("RIGHT", legacy, "LEFT", -4, 0)
    left:SetScript("OnClick", function() nudge(-1) end)

    local right = CreateFrame("Button", nil, legacy, "UIPanelButtonTemplate")
    right:SetSize(18, 18)
    right:SetText(">")
    right:SetPoint("LEFT", legacy, "RIGHT", 4, 0)
    right:SetScript("OnClick", function() nudge(1) end)

    return legacy
end

local function createDropdown(parent, width, current, items, onSelect)
    local dropdown
    if WOW_PROJECT_ID == WOW_PROJECT_MAINLINE then
        dropdown = CreateFrame("DropdownButton", nil, parent, "WowStyle1DropdownTemplate")
    end
    if dropdown and dropdown.SetupMenu then
        dropdown:SetSize(width, 22)
        local idx = 1
        for i, v in ipairs(items or {}) do
            if v == current then idx = i break end
        end
        local function setSelection(val, trigger)
            dropdown.currentValue = val
            dropdown:SetText(val or "")
            if trigger and onSelect then onSelect(val) end
        end
        dropdown:SetupMenu(function(self, root)
            for _, val in ipairs(items or {}) do
                root:CreateCheckbox(val, function() return dropdown.currentValue == val end, function()
                    setSelection(val, true)
                end)
            end
        end)
        setSelection(items and items[idx] or current, false)
        return dropdown
    end

    local drop = CreateFrame("Frame", nil, parent, "UIDropDownMenuTemplate")
    UIDropDownMenu_SetWidth(drop, width)
    UIDropDownMenu_SetText(drop, current)
    UIDropDownMenu_Initialize(drop, function(self, level)
        local info = UIDropDownMenu_CreateInfo()
        for _, item in ipairs(items or {}) do
            info.text = item
            info.func = function()
                UIDropDownMenu_SetText(drop, item)
                if onSelect then onSelect(item) end
                CloseDropDownMenus()
            end
            info.checked = (item == current)
            UIDropDownMenu_AddButton(info, level)
        end
    end)
    return drop
end

if Controls then
    if Controls.createCheck then
        createCheck = Controls.createCheck
    end
    if Controls.createSlider then
        createSlider = function(parent, label, min, max, step, value, onChanged)
            return Controls.createSlider(parent, label, min, max, step, value, onChanged, "Low", "High")
        end
    end
    if Controls.createDropdown then
        createDropdown = Controls.createDropdown
    end
end

local function getAchievementNameIcon(achID)
    local name, _, _, _, _, _, _, _, _, icon = GetAchievementInfo(achID)
    if (not name or name == "") and C_AchievementInfo and C_AchievementInfo.GetAchievementInfo then
        local info = C_AchievementInfo.GetAchievementInfo(achID)
        if info then
            name = info.name or name
            icon = info.icon or icon
        end
    end
    return name, icon
end

local function primeAchievementData(achID)
    if not achID then return end
    Panel._tooltipPrimeCache = Panel._tooltipPrimeCache or (Util and Util.MakeTTLCache and Util:MakeTTLCache())
    if Panel._tooltipPrimeCache and Panel._tooltipPrimeCache.get and Panel._tooltipPrimeCache.get(achID) then
        return
    end
    if C_AchievementInfo and C_AchievementInfo.RequestAchievementInfo then
        C_AchievementInfo.RequestAchievementInfo(achID)
    end
    if C_AchievementInfo and C_AchievementInfo.GetAchievementInfo then
        C_AchievementInfo.GetAchievementInfo(achID)
    end
    local num = GetAchievementNumCriteria and GetAchievementNumCriteria(achID) or 0
    for i = 1, num do
        if GetAchievementCriteriaInfo then
            GetAchievementCriteriaInfo(achID, i)
        end
    end
    if Panel._tooltipPrimeCache and Panel._tooltipPrimeCache.set then
        Panel._tooltipPrimeCache.set(achID, true, 15)
    end
end

local function isRetrievingText(text)
    if not text or text == "" then return false end
    if RETRIEVING_DATA and text == RETRIEVING_DATA then return true end
    local lowered = string.lower(tostring(text))
    return string.find(lowered, "retrieving", 1, true) ~= nil
end

local function showAchievementTooltip(owner, achID, rewardTextOverride)
    if not achID or not owner then return end
    primeAchievementData(achID)
    local id, name, points, completed, month, day, year, description, _, _, rewardText = GetAchievementInfo(achID)
    local title = (name and name ~= "") and name or ("[" .. tostring(achID) .. "]")
    local reward = rewardTextOverride or rewardText
    GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
    GameTooltip:ClearLines()
    GameTooltip:AddLine(title, 1, 0.82, 0)
    if points and points > 0 then
        GameTooltip:AddLine(string.format("%d pts", points), 1, 1, 1)
    end
    if completed then
        if month and day and year and month > 0 then
            GameTooltip:AddLine(string.format("Completed on %02d/%02d/%d", month, day or 0, year or 0), 0, 1, 0)
        else
            GameTooltip:AddLine("Completed", 0, 1, 0)
        end
    else
        GameTooltip:AddLine("Achievement in progress by " .. (UnitName("player") or "you"), 0.5, 0.8, 1)
    end
    if description and description ~= "" then
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(description, 0.9, 0.9, 0.9, true)
    end
    local numC = GetAchievementNumCriteria and GetAchievementNumCriteria(achID) or 0
    if numC > 0 then
        local done, total = 0, numC
        local criteriaShown = 0
        local criteriaPending = false
        for i = 1, numC do
            local critName, _, critCompleted, qty, req = GetAchievementCriteriaInfo(achID, i)
            if critCompleted then
                done = done + 1
            elseif qty and req and req > 0 then
                done = done + (qty / req)
            end
            if not isRetrievingText(critName) then
                local r, g, b = 0.6, 0.6, 0.6
                if critCompleted then r, g, b = 0, 1, 0 end
                local progressText = ""
                if req and req > 1 then progressText = string.format(" (%d/%d)", qty or 0, req) end
                GameTooltip:AddLine("• " .. (critName or ("Criteria " .. i)) .. progressText, r, g, b, true)
                criteriaShown = criteriaShown + 1
            else
                criteriaPending = true
            end
        end
        if criteriaShown > 0 then
            GameTooltip:AddLine(" ")
        elseif criteriaPending then
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine("Loading criteria details...", 0.75, 0.82, 0.95)
        end
        local percent = (done / total) * 100
        GameTooltip:AddDoubleLine("Character progress:", string.format("%d / %d (%.0f%%)", math.floor(done + 0.5), total, percent), 1, 1, 1, 1, 1, 1)
    end
    if reward and reward ~= "" then
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("Reward", 0.6, 0.8, 1)
        GameTooltip:AddLine(reward, 0.9, 0.9, 0.9, true)
    end
    GameTooltip:Show()
end
Panel.primeAchievementData = primeAchievementData
Panel.showAchievementTooltip = showAchievementTooltip

local function onAchievementTooltipEnter(self)
    showAchievementTooltip(self, self._achID, self._rewardText)
end

local function onAchievementTooltipLeave()
    GameTooltip:Hide()
end

local function onAchievementRowClick(self, button)
    if not self or not self._achID then return end
    handleAchievementClick(self._achID, nil, button, self._achName)
end

local function onActionButtonTooltipEnter(self)
    if not (self and self._acaTooltipText and GameTooltip) then return end
    GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
    GameTooltip:SetText(self._acaTooltipText)
    GameTooltip:Show()
end

local function onActionButtonTooltipLeave()
    if GameTooltip then GameTooltip:Hide() end
end

local function onResultIgnoreClick(self)
    local row = self and self.GetParent and self:GetParent()
    local ach = row and row._achData
    if not ach then return end

    local name, icon = ach.name, ach.icon
    if (not name or name == "") or not icon then
        local fetchedName, fetchedIcon = getNameIconWithFallback(ach.id)
        name = name or fetchedName
        icon = icon or fetchedIcon
    end
    rememberIgnoredInfo(ach.id, name, icon)

    if ns.State and ns.State.results then
        for idx = #ns.State.results, 1, -1 do
            if ns.State.results[idx].id == ach.id then
                table.remove(ns.State.results, idx)
                break
            end
        end
        Panel:Render(ns.State.results)
        if Panel.RefreshTop5Override then
            Panel:RefreshTop5Override(ns.State.results)
        elseif ns.Tracker and ns.Tracker.Update then
            ns.Tracker:Update(ns.State.results)
        end
    end

    if Panel.frame and Panel.frame.ignoredFrame and Panel.frame.ignoredFrame:IsShown() then
        Panel:RenderIgnored()
        Panel:RefreshPanelButtons()
    elseif Panel.ShowIgnoredPanel then
        Panel:ShowIgnoredPanel()
    end
end

local function onIgnoredRestoreClick(self)
    local row = self and self.GetParent and self:GetParent()
    local achID = row and row._achIDKey
    if not achID then return end
    if ACAU_Ignore then ACAU_Ignore[achID] = nil end
    if ACAU_Global and ACAU_Global.ignoreAll then ACAU_Global.ignoreAll[achID] = nil end
    if ns.ApplyFilters then ns.ApplyFilters() end
    Panel:RenderIgnored()
end

local function onIgnoredIgnoreAllClick(self)
    local row = self and self.GetParent and self:GetParent()
    if not row then return end
    local achID = row._achIDKey
    if not achID then return end
    rememberIgnoredInfoGlobal(achID, row._achStoredName, row._achStoredIcon)
    if ACAU_Ignore then ACAU_Ignore[achID] = nil end
    if ns.ApplyFilters then ns.ApplyFilters() end
    Panel:RenderIgnored()
end

local function tooltipForRow(row, achID, rewardText)
    if not row then return end
    row._achID = achID
    row._rewardText = rewardText
    if not row._acaTooltipBound then
        row:SetScript("OnEnter", onAchievementTooltipEnter)
        row:SetScript("OnLeave", onAchievementTooltipLeave)
        row._acaTooltipBound = true
    end
end
Panel.tooltipForRow = tooltipForRow

local function buildFiltersPanel(panel)
    if panel.filtersFrame then return end
    local f = CreateFrame("Frame", nil, panel, "BackdropTemplate")
    f:SetWidth(360)
    f:SetPoint("TOPLEFT", panel, "TOPRIGHT", 10, 0)
    f:SetPoint("BOTTOMLEFT", panel, "BOTTOMRIGHT", 10, 0)
    f:SetFrameStrata(panel:GetFrameStrata() or "MEDIUM")
    f:SetFrameLevel((panel:GetFrameLevel() or 10) + 20)
    f:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 8, edgeSize = 12,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    f:SetBackdropColor(0.05, 0.05, 0.05, 0.95)
    f:SetBackdropBorderColor(0.2, 0.2, 0.2, 1)
    f:Hide()

    local child = CreateFrame("Frame", nil, f)
    child:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -8)
    child:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -8, 8)
    local overlay = child:CreateTexture(nil, "BACKGROUND")
    overlay:SetAllPoints()
    overlay:SetColorTexture(0, 0, 0, 0.3)
    child._overlay = overlay

    local header = child:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    header:SetPoint("TOP", child, "TOP", 0, -2)
    header:SetJustifyH("CENTER")
    header:SetText("Achievement Filters")

    local y = -30
    local section = child:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    section:SetPoint("TOPLEFT", child, "TOPLEFT", 0, y)
    section:SetText("Categories")
    local toggleAll = createMainPanelButton(child, "Toggle All", nil)
    toggleAll:SetSize(92, 24)
    toggleAll:SetPoint("LEFT", section, "RIGHT", 8, 0)
    local catChecks = {}
    toggleAll:SetScript("OnClick", function()
        local anyOff = false
        for category, _ in pairs(Const.CATEGORY_DEFAULTS) do
            if category ~= "ProfessionsMode" and category ~= "WorldEventsMode" then
                if ACAU_CategoryDB[category] == false then anyOff = true break end
            end
        end
        local setVal = anyOff and true or false
        for category, _ in pairs(Const.CATEGORY_DEFAULTS) do
            if category ~= "ProfessionsMode" and category ~= "WorldEventsMode" then
                ACAU_CategoryDB[category] = setVal
                local cb = catChecks[category]
                if cb and cb.SetChecked then cb:SetChecked(setVal) end
            end
        end
        if ns.ApplyFilters then ns.ApplyFilters() end
    end)
    y = y - 18
    local col = 0
    for category, default in pairs(Const.CATEGORY_DEFAULTS) do
        if category ~= "ProfessionsMode" and category ~= "WorldEventsMode" then
            local cb = createCheck(child, category, ACAU_CategoryDB[category] ~= false, function(val)
                ACAU_CategoryDB[category] = val
                if ns.ApplyFilters then ns.ApplyFilters() end
            end)
            catChecks[category] = cb
            cb:SetPoint("TOPLEFT", child, "TOPLEFT", col * 130, y)
            col = col + 1
            if col >= 2 then col = 0; y = y - 24 end
        end
    end
    y = y - 26

    local profLabel = child:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    profLabel:SetPoint("TOPLEFT", child, "TOPLEFT", 0, y - 10)
    profLabel:SetText("Professions")
    local profDrop = createDropdown(child, 120, ACAU_CategoryDB["ProfessionsMode"] or "Learned", { "All", "Learned", "None" }, function(mode)
        ACAU_CategoryDB["ProfessionsMode"] = mode
        if ns.ApplyFilters then ns.ApplyFilters() end
    end)
    profDrop:SetPoint("TOPLEFT", profLabel, "BOTTOMLEFT", 4, -4)
    local worldLabel = child:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    worldLabel:SetPoint("TOPLEFT", child, "TOPLEFT", 150, y - 10)
    worldLabel:SetText("World Events")
    local worldDrop = createDropdown(child, 120, ACAU_CategoryDB["WorldEventsMode"] or "Active", { "All", "Active", "None" }, function(mode)
        ACAU_CategoryDB["WorldEventsMode"] = mode
        if ns.ApplyFilters then ns.ApplyFilters() end
    end)
    worldDrop:SetPoint("TOPLEFT", worldLabel, "BOTTOMLEFT", 4, -4)
    y = y - 64

    local expHeader = child:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    expHeader:SetPoint("TOPLEFT", child, "TOPLEFT", 0, y)
    expHeader:SetText("Expansions")
    y = y - 18
    col = 0
    for _, expName in ipairs({
        "Classic", "The Burning Crusade", "Wrath of the Lich King", "Cataclysm",
        "Mists of Pandaria", "Warlords of Draenor", "Legion", "Battle for Azeroth",
        "Shadowlands", "Dragonflight", "The War Within", "Midnight",
    }) do
        local cb = createCheck(child, expName, ACAU_ExpFilterDB[expName], function(val)
            ACAU_ExpFilterDB[expName] = val
            if Remix and Remix.OnExpansionChanged then Remix:OnExpansionChanged() end
            if ns.ApplyFilters then ns.ApplyFilters() end
        end)
        cb:SetPoint("TOPLEFT", child, "TOPLEFT", col * 140, y)
        col = col + 1
        if col >= 2 then col = 0; y = y - 24 end
    end

    y = y - 5

    local thresh = createSlider(child, "Completion threshold", 0, 99, 1, ACAU_Config.threshold or 80, function(v)
        ACAU_Config.threshold = v
        if ns.ApplyFilters then ns.ApplyFilters() end
    end)
    thresh:SetPoint("TOP", child, "TOP", 0, y - 30)
    thresh:SetWidth((child:GetWidth() or 0) - 24)
    y = y - 60

    child:SetHeight(math.abs(y) + 40)

    f:SetScript("OnHide", function()
        if panel.filtersButton then
            panel.filtersButton:SetText("Filters")
        end
    end)

    panel.filtersFrame = f
    if ns.Panel and ns.Panel.ApplyTheme then
        ns.Panel:ApplyTheme()
    end
end

function Panel:ToggleFilters()
    if not self.frame then return end
    buildFiltersPanel(self.frame)
    local f = self.frame.filtersFrame
    if not f then return end
    if self.frame.ignoredFrame and self.frame.ignoredFrame:IsShown() then
        self.frame.ignoredFrame:Hide()
    end
    if self.frame.metaFrame and self.frame.metaFrame:IsShown() then
        self.frame.metaFrame:Hide()
    end
    if f:IsShown() then
        f:Hide()
    else
        f:Show()
    end
    if self.SaveViewState then self:SaveViewState() end
    self:RefreshPanelButtons()
end

function Panel:RefreshFiltersUI()
    if not self.frame then return end
    if not self.frame.filtersFrame then return end
    local wasShown = self.frame.filtersFrame:IsShown()
    self.frame.filtersFrame:Hide()
    self.frame.filtersFrame:SetParent(nil)
    self.frame.filtersFrame = nil
    buildFiltersPanel(self.frame)
    if wasShown and self.frame.filtersFrame then
        self.frame.filtersFrame:Show()
    end
end

local function makeMetaCard(parent)
    local card = CreateFrame("Button", nil, parent, "BackdropTemplate")
    card:SetSize(160, 160)
    card:RegisterForClicks("AnyUp")

    card.bg = card:CreateTexture(nil, "BACKGROUND")
    card.bg:SetAllPoints()
    card.bg:SetColorTexture(0, 0, 0, 0.18)

    card.border = CreateFrame("Frame", nil, card, "BackdropTemplate")
    card.border:SetAllPoints()
    card.border:SetFrameLevel(card:GetFrameLevel() + 1)
    card.border:SetBackdrop({
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 12,
        insets = { left = 2, right = 2, top = 2, bottom = 2 },
    })
    card.border:SetBackdropBorderColor(0.3, 0.3, 0.3, 0.8)

    card.highlight = card:CreateTexture(nil, "HIGHLIGHT")
    card.highlight:SetAllPoints()
    card.highlight:SetColorTexture(1, 1, 1, 0.08)

    card.Title = card:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    card.Title:SetPoint("TOPLEFT", card, "TOPLEFT", 8, -8)
    card.Title:SetPoint("TOPRIGHT", card, "TOPRIGHT", -8, -8)
    card.Title:SetJustifyH("CENTER")
    card.Title:SetWordWrap(true)

    card.Icon = card:CreateTexture(nil, "ARTWORK")
    card.Icon:SetSize(56, 56)
    card.Icon:SetPoint("TOP", card, "TOP", 0, -36)

    card.Name = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    card.Name:SetPoint("TOP", card, "TOP", 0, -96)
    card.Name:SetWidth(120)
    card.Name:SetJustifyH("CENTER")
    card.Name:SetWordWrap(true)

    card.NameBorder = CreateFrame("Frame", nil, card, "BackdropTemplate")
    card.NameBorder:SetPoint("TOPLEFT", card.Name, "TOPLEFT", -6, 4)
    card.NameBorder:SetPoint("BOTTOMRIGHT", card.Name, "BOTTOMRIGHT", 6, -4)
    card.NameBorder:SetFrameLevel(card:GetFrameLevel() + 2)
    card.NameBorder:SetBackdrop({
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 10,
        insets = { left = 2, right = 2, top = 2, bottom = 2 },
    })
    card.NameBorder:SetBackdropBorderColor(0.25, 0.25, 0.25, 0.9)
    card.NameBorder:EnableMouse(false)

    card.Percent = card:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    card.Percent:SetPoint("BOTTOM", card, "BOTTOM", 0, 23)
    card.Percent:SetJustifyH("CENTER")

    return card
end

function Panel:RenderMetaLanding()
    if not (self.frame and self.frame.metaChild and Meta and Meta.GetMetas) then return end
    self._metaMode = "landing"
    self._metaSelectedID = nil
    local parent = self.frame.metaChild
    local cards = parent._metaCards or {}
    for _, c in ipairs(cards) do c:Hide() end
    if parent._rows then
        for _, r in ipairs(parent._rows) do r:Hide() end
    end
    if parent._metaEmpty then parent._metaEmpty:Hide() end
    parent._metaCards = cards
    if self.frame and self.frame.metaFrame and self.frame.metaFrame:IsShown() then
        self:SaveViewState()
    end

    local themeKey = (ACAU_Config and ACAU_Config.theme) or Const.DEFAULTS.theme
    local palette = getPalette(themeKey)
    local style = getThemeStyle and getThemeStyle(themeKey)
    local nameR, nameG, nameB = 1, 0.82, 0
    local detailR, detailG, detailB = 0.9, 0.9, 0.9
    local accentR, accentG, accentB = 1, 0.78, 0.2
    local borderR, borderG, borderB, borderA = 0.3, 0.3, 0.3, 0.8
    local innerBorderR, innerBorderG, innerBorderB, innerBorderA = 0.25, 0.25, 0.25, 0.9
    if palette then
        if palette.textPrimary then nameR, nameG, nameB = unpack(palette.textPrimary) end
        if palette.textSecondary then detailR, detailG, detailB = unpack(palette.textSecondary) end
        if palette.accent then accentR, accentG, accentB = unpack(palette.accent) end
    end
    if style and style.borderColor then
        borderR = style.borderColor[1] or borderR
        borderG = style.borderColor[2] or borderG
        borderB = style.borderColor[3] or borderB
        borderA = style.borderColor[4] or borderA
        innerBorderR, innerBorderG, innerBorderB, innerBorderA = borderR, borderG, borderB, borderA
    elseif palette and palette.accent then
        borderR, borderG, borderB = accentR, accentG, accentB
        innerBorderR, innerBorderG, innerBorderB = accentR, accentG, accentB
    end

    if self.frame.metaFrame then
        self.frame.metaFrame.backBtn:Hide()
        if self.frame.metaFrame.header then
            self.frame.metaFrame.header:ClearAllPoints()
            self.frame.metaFrame.header:SetPoint("TOP", self.frame.metaFrame, "TOP", 0, -12)
            self.frame.metaFrame.header:SetJustifyH("CENTER")
            self.frame.metaFrame.header:SetText("Expansion Meta Achievements")
        end
    end

    local data = Meta:GetMetas() or {}
    local order = {
        "Midnight",
        "The War Within",
        "Dragonflight",
        "Shadowlands",
        "Battle for Azeroth",
        "Legion",
        "Warlords of Draenor",
    }

    local baseW = parent:GetWidth()
    if not baseW or baseW <= 0 then baseW = 320 end
    local cardW = math.floor((baseW - 8) / 2)
    local cardIdx = 1

    for idx, expName in ipairs(order) do
        local metas = data[expName]
        local meta = metas and metas[1]
        local title = expName
        local body = meta and meta.name or "Coming Soon"
        local percentValue = meta and (meta.avg_percent or meta.percent) or 0
        local percentText = meta and (meta.completed and "Complete!" or string.format("%.0f%%", percentValue)) or ""
        local icon = meta and meta.icon or 134400
        local completed = meta and meta.completed

        local card = cards[cardIdx] or makeMetaCard(parent)
        cards[cardIdx] = card
        cardIdx = cardIdx + 1
        card:Show()
        card:SetSize(cardW, cardW)
        local col = (idx - 1) % 2
        local row = math.floor((idx - 1) / 2)
        local xOffset = col * (cardW + 8)
        local yOffset = -4 - (row * (cardW + 8))
        card:ClearAllPoints()
        card:SetPoint("TOPLEFT", parent, "TOPLEFT", xOffset, yOffset)
        card.Icon:SetTexture(icon)
        if card.Icon.SetDesaturated then
            card.Icon:SetDesaturated(meta and meta.completed or false)
        end
        if card.border and card.border.SetBackdropBorderColor then
            card.border:SetBackdropBorderColor(borderR, borderG, borderB, borderA)
        end
        if card.Title then
            card.Title:SetText(title)
            card.Title:SetTextColor(nameR, nameG, nameB)
        end
        card.Name:SetText(body)
        card.Name:SetTextColor(detailR, detailG, detailB)
        if card.NameBorder and card.NameBorder.SetBackdropBorderColor then
            card.NameBorder:SetBackdropBorderColor(innerBorderR, innerBorderG, innerBorderB, innerBorderA * 0.9)
        end
        card.Percent:SetText(percentText)
        card.Name:EnableMouse(meta and meta.id ~= nil)
        if not card.TopCheck then
            local cb = CreateFrame("CheckButton", nil, card, "UICheckButtonTemplate")
            cb:SetSize(18, 18)
            cb:SetPoint("BOTTOMLEFT", card, "BOTTOMLEFT", 6, 6)
            cb.text = cb.Text or cb:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            cb.text:SetPoint("LEFT", cb, "RIGHT", 2, 0)
            cb.text:SetJustifyH("LEFT")
            cb.text:SetText("Track in Top 5")
            card.TopCheck = cb
        end
        if card.TopCheck then
            card.TopCheck.metaID = meta and meta.id or nil
            card.TopCheck:SetChecked(meta and meta.id and ACAU_Config and ACAU_Config.metaTopOverride == meta.id)
            card.TopCheck:SetEnabled(meta and meta.id ~= nil)
            card.TopCheck.Text:SetText("Track in Top 5")
            card.TopCheck:SetScript("OnClick", function(btn)
                if not meta or not meta.id then
                    btn:SetChecked(false)
                    return
                end
                if btn:GetChecked() then
                    ACAU_Config.metaTopOverride = meta.id
                    ACAU_Config.metaTopOverrideName = meta.name
                else
                    if ACAU_Config.metaTopOverride == meta.id then
                        ACAU_Config.metaTopOverride = nil
                        ACAU_Config.metaTopOverrideName = nil
                    end
                end
                for _, other in ipairs(cards) do
                    if other and other.TopCheck and other.TopCheck ~= btn then
                        local mid = other.TopCheck.metaID
                        other.TopCheck:SetChecked(mid and ACAU_Config.metaTopOverride == mid)
                    end
                end
                self:RefreshTop5Override()
            end)
        end
        if not card.NameClick then
            local btn = CreateFrame("Button", nil, card)
            btn:RegisterForClicks("AnyUp")
            btn:SetPoint("TOPLEFT", card.Name, "TOPLEFT", -6, 0)
            btn:SetPoint("BOTTOMRIGHT", card.Name, "BOTTOMRIGHT", 6, 0)
            btn:SetFrameLevel(card:GetFrameLevel() + 5)
            btn:SetAlpha(0)
            card.NameClick = btn
        end
        card.NameClick:RegisterForClicks("AnyUp")
        card.NameClick:EnableMouse(meta and meta.id ~= nil)
        if meta and meta.id then
            card.NameClick:SetScript("OnClick", function(_, button)
                handleAchievementClick(meta.id, nil, button, meta.name)
            end)
            card.NameClick:SetScript("OnEnter", function(self)
                showAchievementTooltip(self, meta.id)
            end)
            card.NameClick:SetScript("OnLeave", function() GameTooltip:Hide() end)
        else
            card.NameClick:SetScript("OnClick", nil)
            card.NameClick:SetScript("OnEnter", nil)
            card.NameClick:SetScript("OnLeave", nil)
        end
        if completed then
            card.Percent:SetTextColor(detailR, detailG, detailB)
            card.bg:SetColorTexture(0, 0, 0, 0.12)
        else
            card.Percent:SetTextColor(accentR, accentG, accentB)
            card.bg:SetColorTexture(0, 0, 0, 0.18)
        end
        if meta and meta.id then
            card:SetScript("OnClick", function()
                self:RenderMetaDetail(meta.id, meta.name)
            end)
        else
            card:SetScript("OnClick", nil)
        end
    end

    local rows = math.ceil(#order / 2)
    parent:SetHeight((rows * (cardW + 8)) + 20)
end

function Panel:RenderMetaDetail(metaID, metaName)
    if not (self.frame and self.frame.metaChild and Meta and Meta.GetMetaDetail) then return end
    self._metaMode = "detail"
    self._metaSelectedID = metaID
    local parent = self.frame.metaChild
    if parent._metaHeaders then
        for _, h in ipairs(parent._metaHeaders) do h:Hide() end
    end
    if parent._metaCards then
        for _, c in ipairs(parent._metaCards) do c:Hide() end
    end
    local rows = parent._rows or {}
    for _, r in ipairs(rows) do r:Hide() end
    parent._rows = rows
    if self.frame and self.frame.metaFrame and self.frame.metaFrame:IsShown() then
        self:SaveViewState()
    end

    if self.frame.metaFrame then
        self.frame.metaFrame.backBtn:Show()
        self.frame.metaFrame.backBtn:SetScript("OnClick", function()
            self:RenderMetaLanding()
        end)
        if self.frame.metaFrame.header then
            self.frame.metaFrame.header:ClearAllPoints()
            self.frame.metaFrame.header:SetPoint("TOP", self.frame.metaFrame, "TOP", 0, -12)
            self.frame.metaFrame.header:SetJustifyH("CENTER")
            self.frame.metaFrame.header:SetText(metaName or "Meta Detail")
        end
    end

    local palette = getPalette((ACAU_Config and ACAU_Config.theme) or Const.DEFAULTS.theme)
    local nameR, nameG, nameB = 1, 0.82, 0
    local detailR, detailG, detailB = 0.9, 0.9, 0.9
    local accentR, accentG, accentB = 1, 0.78, 0.2
    local accent2R, accent2G, accent2B = 0.55, 0.78, 1
    local highlightR, highlightG, highlightB, highlightA = 1, 1, 1, 0.25
    local dividerA = 0.12
    if palette then
        if palette.textPrimary then nameR, nameG, nameB = unpack(palette.textPrimary) end
        if palette.textSecondary then detailR, detailG, detailB = unpack(palette.textSecondary) end
        if palette.accent then accentR, accentG, accentB = unpack(palette.accent) end
        if palette.accentAlt then accent2R, accent2G, accent2B = unpack(palette.accentAlt) end
        if palette.highlight then
            highlightR, highlightG, highlightB, highlightA = palette.highlight[1], palette.highlight[2], palette.highlight[3], palette.highlight[4] or highlightA
        end
        if palette.dividerAlpha then dividerA = palette.dividerAlpha end
    end

    local filter = Meta:GetDetailFilter()
    if self.frame.metaFrame and self.frame.metaFrame.filterDrop then
        local drop = self.frame.metaFrame.filterDrop
        if drop.SetText then
            drop:SetText(filter or "All")
        elseif UIDropDownMenu_SetText then
            UIDropDownMenu_SetText(drop, filter or "All")
        end
    end

    local data = Meta:GetMetaDetail(metaID) or {}
    local filtered = {}
    for _, node in ipairs(data) do
        if filter == "Incomplete" and node.completed then
        elseif filter == "Completed" and not node.completed then
        else
            table.insert(filtered, node)
        end
    end

    local y = -4
    local total = #filtered
    for i, ach in ipairs(filtered) do
        local row = rows[i] or makeRow(parent)
        rows[i] = row
        row:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, y)
        row:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -4, y)
        if row.bg then
            local evenA = palette and palette.rowEvenAlpha or 0.32
            local oddA = palette and palette.rowOddAlpha or 0.22
            if i % 2 == 0 then
                row.bg:SetColorTexture(0, 0, 0, evenA)
            else
                row.bg:SetColorTexture(0, 0, 0, oddA)
            end
        end
        row.Icon:SetTexture(ach.icon or 134400)
        if row.Icon.SetDesaturated then
            row.Icon:SetDesaturated(ach.completed)
        end
        if row.Icon.SetVertexColor then
            if ach.completed then
                row.Icon:SetVertexColor(0.7, 0.7, 0.7)
            else
                row.Icon:SetVertexColor(1, 1, 1)
            end
        end
        row.Name:SetText(ach.name or ("[" .. tostring(ach.id) .. "]"))
        row.Percent:SetText(ach.completed and "Complete!" or ("%.0f%%"):format(ach.percent or 0))
        row.Reward:SetText("")
        row.Points:SetText(ach.points and ach.points > 0 and (ach.points .. " pts") or "")
        if row.Divider then
            if i < total then
                row.Divider:SetColorTexture(1, 1, 1, dividerA)
                row.Divider:Show()
            else
                row.Divider:Hide()
            end
        end
        if row.Name then
            if ach.completed then
                row.Name:SetTextColor(detailR, detailG, detailB)
            else
                row.Name:SetTextColor(nameR, nameG, nameB)
            end
        end
        if row.Percent then
            if ach.completed then
                row.Percent:SetTextColor(detailR, detailG, detailB)
            else
                row.Percent:SetTextColor(accentR, accentG, accentB)
            end
        end
        if row.Reward then row.Reward:SetTextColor(detailR, detailG, detailB) end
        if row.Points then
            if ach.completed then
                row.Points:SetTextColor(detailR, detailG, detailB)
            else
                row.Points:SetTextColor(accent2R, accent2G, accent2B)
            end
        end
        if row.highlight then
            row.highlight:SetVertexColor(highlightR, highlightG, highlightB, highlightA)
        end

        tooltipForRow(row, ach.id)
        row._achID = ach.id
        row._achName = ach.name
        if not row._acaMainBound then
            row:SetScript("OnClick", onAchievementRowClick)
            row._acaMainBound = true
        end
        row.Ignore:SetScript("OnClick", nil)
        row.Ignore:Hide()
        row.IgnoreAll:SetScript("OnClick", nil)
        row.IgnoreAll:Hide()

        row:Show()
        y = y - 52
    end
    parent:SetHeight(math.abs(y) + 20)
end

local buildMetaPanel
local function buildMetaPanelInternal(panel)
    if not Util:IsRetail() then return end
    if panel.metaFrame then return end

    local f = CreateFrame("Frame", nil, panel, "BackdropTemplate")
    f:SetWidth(360)
    f:SetPoint("TOPLEFT", panel, "TOPRIGHT", 10, 0)
    f:SetPoint("BOTTOMLEFT", panel, "BOTTOMRIGHT", 10, 0)
    f:SetFrameStrata(panel:GetFrameStrata() or "MEDIUM")
    f:SetFrameLevel((panel:GetFrameLevel() or 10) + 20)
    f:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 8, edgeSize = 12,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    f:SetBackdropColor(0.05, 0.05, 0.05, 0.95)
    f:SetBackdropBorderColor(0.2, 0.2, 0.2, 1)
    f:Hide()

    local child = CreateFrame("Frame", nil, f)
    child:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -8)
    child:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -8, 8)
    local overlay = child:CreateTexture(nil, "BACKGROUND")
    overlay:SetAllPoints()
    overlay:SetColorTexture(0, 0, 0, 0.3)
    child._overlay = overlay

    local backBtn = createMainPanelButton(child, "Back", function()
        if ns.Panel and ns.Panel.RenderMetaLanding then
            ns.Panel:RenderMetaLanding()
        end
    end)
    backBtn:SetSize(56, 24)
    backBtn:SetPoint("TOPLEFT", child, "TOPLEFT", 0, -2)
    backBtn:Hide()
    f.backBtn = backBtn

    local header = child:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    header:SetPoint("TOP", child, "TOP", 0, -2)
    header:SetJustifyH("CENTER")
    header:SetText("Expansion Meta Achievements")
    f.header = header

    local scroll = CreateFrame("ScrollFrame", nil, child, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", child, "TOPLEFT", 0, -26)
    scroll:SetPoint("BOTTOMRIGHT", child, "BOTTOMRIGHT", -24, 0)
    local scrollChild = CreateFrame("Frame", nil, scroll)
    scrollChild:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0)
    scrollChild:SetPoint("RIGHT", scroll, "RIGHT", 0, 0)
    scrollChild:SetHeight(600)
    scrollChild:SetWidth(320)
    scroll:SetScrollChild(scrollChild)
    scroll:HookScript("OnSizeChanged", function(self, w)
        if scrollChild and w and w > 0 then
            scrollChild:SetWidth(w - 2)
        end
    end)
    initLegacyScrollFrame(scroll, RESULT_ROW_EXTENT)

    panel.metaFrame = f
    panel.metaChild = scrollChild

    if ns.Panel and ns.Panel.ApplyTheme then
        ns.Panel:ApplyTheme()
    end
end

buildMetaPanel = buildMetaPanelInternal

function Panel:ToggleMeta()
    if not Util:IsRetail() then return end
    if not self.frame then return end
    buildMetaPanel(self.frame)
    local f = self.frame.metaFrame
    if not f then return end
    if self.frame.filtersFrame and self.frame.filtersFrame:IsShown() then
        self.frame.filtersFrame:Hide()
    end
    if self.frame.ignoredFrame and self.frame.ignoredFrame:IsShown() then
        self.frame.ignoredFrame:Hide()
    end
    if f:IsShown() then
        f:Hide()
    else
        f:Show()
        self:RenderMetaLanding()
    end
    if self.SaveViewState then self:SaveViewState() end
    self:RefreshPanelButtons()
end

getNameIconWithFallback = function(achID)
    if not achID then return nil, nil end
    if UIParentLoadAddOn then UIParentLoadAddOn("Blizzard_AchievementUI") end
    primeAchievementData(achID)
    local name, _, _, _, _, _, _, _, _, icon = GetAchievementInfo(achID)
    if (not name or name == "") and C_AchievementInfo and C_AchievementInfo.GetAchievementInfo then
        local info = C_AchievementInfo.GetAchievementInfo(achID)
        if info then
            name = info.name or name
            icon = info.icon or icon
        end
    end
    return name, icon
end

local pendingAchievementNames = {}
local pendingIgnoredTimer
local THEMES = {
    Modern = {
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        bgColor = { 0.06, 0.06, 0.06, 0.94 },
        borderColor = { 0.22, 0.22, 0.22, 1 },
        tile = true,
        palette = {
            textPrimary = { 1, 0.82, 0 },
            textSecondary = { 0.9, 0.9, 0.9 },
            accent = { 1, 0.78, 0.2 },
            accentAlt = { 0.55, 0.78, 1 },
            rowEvenAlpha = 0.32,
            rowOddAlpha = 0.22,
            highlight = { 1, 0.82, 0, 0.22 },
            dividerAlpha = 0.12,
        },
    },
    Parchment = {
        bgFile = "Interface\\AchievementFrame\\UI-Achievement-Parchment-Horizontal",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        bgColor = { 1, 1, 1, 1 },
        borderColor = { 0.2, 0.15, 0.1, 1 },
        tile = false,
        palette = {
            accent = { 1, 1, 1 },
            accentAlt = { 1, 1, 1 },
            rowEvenAlpha = 0.42,
            rowOddAlpha = 0.32,
            dividerAlpha = 0.10,
        },
    },
    Stone = {
        bgFile = "Interface\\FrameGeneral\\UI-Background-Rock",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        bgColor = { 1, 1, 1, 1 },
        borderColor = { 0.75, 0.75, 0.75, 1 },
        tile = false,
        palette = {
            textPrimary = { 0.95, 0.95, 0.95 },
            textSecondary = { 0.82, 0.82, 0.82 },
            accent = { 0.95, 0.82, 0.6 },
            accentAlt = { 0.65, 0.65, 0.65 },
            rowEvenAlpha = 0.30,
            rowOddAlpha = 0.22,
            highlight = { 0.95, 0.82, 0.6, 0.22 },
            dividerAlpha = 0.14,
        },
    },
    Solarized = {
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
        bgColor = { 0.0, 0.17, 0.21, 0.98 },
        borderColor = { 0.65, 0.74, 0.70, 1 },
        tile = true,
        palette = {
            textPrimary = { 0.82, 0.87, 0.83 },
            textSecondary = { 0.65, 0.74, 0.70 },
            accent = { 0.99, 0.75, 0.33 },
            accentAlt = { 0.42, 0.62, 0.64 },
            rowEvenAlpha = 0.30,
            rowOddAlpha = 0.23,
            highlight = { 0.99, 0.75, 0.33, 0.18 },
            dividerAlpha = 0.12,
        },
    },
    Midnight = {
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        bgColor = { 0.07, 0.07, 0.12, 0.98 },
        borderColor = { 0.4, 0.55, 0.85, 1 },
        tile = true,
        palette = {
            textPrimary = { 0.9, 0.95, 1 },
            textSecondary = { 0.75, 0.82, 0.95 },
            accent = { 0.55, 0.78, 1 },
            accentAlt = { 0.35, 0.5, 0.9 },
            rowEvenAlpha = 0.34,
            rowOddAlpha = 0.25,
            highlight = { 0.55, 0.78, 1, 0.20 },
            dividerAlpha = 0.14,
        },
    },
    Emerald = {
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        bgColor = { 0.04, 0.13, 0.10, 0.98 },
        borderColor = { 0.1, 0.36, 0.28, 1 },
        tile = true,
        palette = {
            textPrimary = { 0.82, 1, 0.88 },
            textSecondary = { 0.64, 0.88, 0.73 },
            accent = { 0.28, 0.74, 0.52 },
            accentAlt = { 0.18, 0.52, 0.38 },
            rowEvenAlpha = 0.32,
            rowOddAlpha = 0.24,
            highlight = { 0.28, 0.74, 0.52, 0.22 },
            dividerAlpha = 0.12,
        },
    },
    ["Fel Remix"] = {
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        bgColor = { 0.04, 0.11, 0.05, 0.98 },
        borderColor = { 0.4706, 1.0, 0.4745, 1 },
        tile = true,
        palette = {
            textPrimary = { 0.88, 0.74, 0.56 },
            textSecondary = { 0.76, 0.62, 0.48 },
            accent = { 0.62, 0.96, 0.28 },
            accentAlt = { 0.42, 0.78, 0.18 },
            rowEvenAlpha = 0.34,
            rowOddAlpha = 0.24,
            highlight = { 0.62, 0.96, 0.28, 0.22 },
            dividerAlpha = 0.14,
        },
    },
    Ruby = {
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
        bgColor = { 0.12, 0.02, 0.02, 0.98 },
        borderColor = { 0.6, 0.12, 0.12, 1 },
        tile = true,
        palette = {
            textPrimary = { 1, 0.88, 0.88 },
            textSecondary = { 0.95, 0.76, 0.76 },
            accent = { 0.95, 0.28, 0.28 },
            accentAlt = { 0.78, 0.18, 0.18 },
            rowEvenAlpha = 0.30,
            rowOddAlpha = 0.22,
            highlight = { 0.95, 0.28, 0.28, 0.20 },
            dividerAlpha = 0.12,
        },
    },
    Sapphire = {
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        bgColor = { 0.02, 0.08, 0.16, 0.98 },
        borderColor = { 0.18, 0.44, 0.72, 1 },
        tile = true,
        palette = {
            textPrimary = { 0.85, 0.93, 1 },
            textSecondary = { 0.72, 0.82, 0.92 },
            accent = { 0.32, 0.65, 0.95 },
            accentAlt = { 0.20, 0.48, 0.78 },
            rowEvenAlpha = 0.32,
            rowOddAlpha = 0.24,
            highlight = { 0.32, 0.65, 0.95, 0.22 },
            dividerAlpha = 0.14,
        },
    },
    Citrine = {
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
        bgColor = { 0.16, 0.06, 0.02, 0.98 },
        borderColor = { 0.8, 0.45, 0.2, 1 },
        tile = true,
        palette = {
            textPrimary = { 1, 0.92, 0.82 },
            textSecondary = { 0.95, 0.82, 0.70 },
            accent = { 1, 0.64, 0.32 },
            accentAlt = { 0.88, 0.48, 0.22 },
            rowEvenAlpha = 0.32,
            rowOddAlpha = 0.24,
            highlight = { 1, 0.64, 0.32, 0.22 },
            dividerAlpha = 0.12,
        },
    },
    Amethyst = {
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        bgColor = { 0.07, 0.02, 0.12, 0.98 },
        borderColor = { 0.52, 0.36, 0.82, 1 },
        tile = true,
        palette = {
            textPrimary = { 0.92, 0.88, 1 },
            textSecondary = { 0.80, 0.76, 0.95 },
            accent = { 0.68, 0.48, 1 },
            accentAlt = { 0.48, 0.32, 0.85 },
            rowEvenAlpha = 0.32,
            rowOddAlpha = 0.24,
            highlight = { 0.68, 0.48, 1, 0.22 },
            dividerAlpha = 0.12,
        },
    },
}


THEMES.Arcane = THEMES.Amethyst
THEMES.Ocean = THEMES.Sapphire
THEMES.Sunset = THEMES.Citrine

local function normalizeThemeKey(key)
    if key == "Ocean" then return "Sapphire" end
    if key == "Sunset" then return "Citrine" end
    return key
end

local function buildCustomTheme()
    local base = THEMES.Modern
    local cfg = (ACAU_Config and ACAU_Config.customTheme) or Const.DEFAULTS.customTheme or {}
    local function pick(key, fallback)
        return (cfg and cfg[key]) or fallback
    end
    local accent = pick("accent", base.palette.accent)
    local style = {
        bgFile = base.bgFile,
        edgeFile = base.edgeFile,
        bgColor = pick("bgColor", base.bgColor),
        borderColor = pick("borderColor", base.borderColor),
        tile = true,
        palette = {
            textPrimary = pick("textPrimary", base.palette.textPrimary),
            textSecondary = pick("textSecondary", base.palette.textSecondary),
            accent = accent,
            accentAlt = pick("accentAlt", base.palette.accentAlt),
            rowEvenAlpha = base.palette.rowEvenAlpha,
            rowOddAlpha = base.palette.rowOddAlpha,
            highlight = { (accent and accent[1]) or 0.32, (accent and accent[2]) or 0.65, (accent and accent[3]) or 0.95, 0.22 },
            dividerAlpha = base.palette.dividerAlpha,
        },
    }
    return style
end

getThemeStyle = function(themeKey)
    themeKey = normalizeThemeKey(themeKey)
    if themeKey == "Custom" then
        return buildCustomTheme()
    end
    return THEMES[themeKey] or THEMES.Modern
end

function getPalette(themeKey)
    local style = getThemeStyle(themeKey)
    return style.palette or THEMES.Modern.palette
end

applyBackdrop = function(frame, style, themeKey)
    if not (frame and frame.SetBackdrop) then return end
    if not style then return end
    local key = themeKey or (ACAU_Config and ACAU_Config.theme) or Const.DEFAULTS.theme
    local alphaMult = 1
    if key ~= "Parchment" and key ~= "Stone" then
        alphaMult = (ACAU_Config and ACAU_Config.themeAlpha) or Const.DEFAULTS.themeAlpha or 1
    end
    frame:SetBackdrop({
        bgFile = style.bgFile or "Interface\\Buttons\\WHITE8X8",
        edgeFile = style.edgeFile or "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = style.tile ~= false,
        tileSize = 8,
        edgeSize = 12,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    if style.bgColor then
        local r, g, b, a = style.bgColor[1] or 1, style.bgColor[2] or 1, style.bgColor[3] or 1, style.bgColor[4] or 1
        frame:SetBackdropColor(r, g, b, a * alphaMult)
    end
    if style.borderColor then
        local r, g, b, a = style.borderColor[1] or 1, style.borderColor[2] or 1, style.borderColor[3] or 1, style.borderColor[4] or 1
        frame:SetBackdropBorderColor(r, g, b, a * alphaMult)
    end
end

local function tintFontsRecursive(frame, r, g, b)
    if not frame then return end
    if frame.Text and frame.Text.SetTextColor then
        frame.Text:SetTextColor(r, g, b)
    end
    if frame.GetRegions then
        for i = 1, select("#", frame:GetRegions()) do
            local region = select(i, frame:GetRegions())
            if region and region.GetObjectType and region:GetObjectType() == "FontString" then
                region:SetTextColor(r, g, b)
            end
        end
    end
    if frame.GetChildren then
        for _, child in ipairs({ frame:GetChildren() }) do
            tintFontsRecursive(child, r, g, b)
        end
    end
end

local function tintCheckboxLabels(frame, r, g, b)
    if not frame or not frame.GetChildren then return end
    for _, child in ipairs({ frame:GetChildren() }) do
        if child and child.GetObjectType and child:GetObjectType() == "CheckButton" then
            local text = child.Text or (child.GetFontString and child:GetFontString())
            if text and text.SetTextColor then
                text:SetTextColor(r, g, b)
            end
        end
        tintCheckboxLabels(child, r, g, b)
    end
end

local function storedIgnoredInfo(achID)
    local entry = (ACAU_Ignore and ACAU_Ignore[achID]) or (ACAU_Global and ACAU_Global.ignoreAll and ACAU_Global.ignoreAll[achID])
    if type(entry) == "table" then
        return entry.name, entry.icon
    elseif type(entry) == "string" then
        return entry, nil
    end
end

rememberIgnoredInfo = function(achID, name, icon)
    ACAU_Ignore = ACAU_Ignore or {}
    if not achID then return end
    if not (name and name ~= "" or icon) then
        ACAU_Ignore[achID] = true
        return
    end
    local entry = type(ACAU_Ignore and ACAU_Ignore[achID]) == "table" and ACAU_Ignore[achID] or {}
    if name and name ~= "" then entry.name = name end
    if icon then entry.icon = icon end
    ACAU_Ignore[achID] = entry
end

rememberIgnoredInfoGlobal = function(achID, name, icon)
    ACAU_Global = ACAU_Global or { ignoreAll = {} }
    ACAU_Global.ignoreAll = ACAU_Global.ignoreAll or {}
    if not achID then return end
    if not (name and name ~= "" or icon) then
        ACAU_Global.ignoreAll[achID] = true
        return
    end
    local entry = type(ACAU_Global.ignoreAll[achID]) == "table" and ACAU_Global.ignoreAll[achID] or {}
    if name and name ~= "" then entry.name = name end
    if icon then entry.icon = icon end
    ACAU_Global.ignoreAll[achID] = entry
end

local function collectIgnoredEntries()
    local ignored = {}
    local seen = {}
    for id in pairs(ACAU_Ignore or {}) do
        local key = tonumber(id) or id
        seen[key] = { id = key, scope = "char" }
    end
    for id in pairs((ACAU_Global and ACAU_Global.ignoreAll) or {}) do
        local key = tonumber(id) or id
        if seen[key] then
            seen[key].scope = "both"
        else
            seen[key] = { id = key, scope = "global" }
        end
    end
    for _, entry in pairs(seen) do
        table.insert(ignored, entry)
    end
    table.sort(ignored, function(a, b)
        local na, nb = tonumber(a.id), tonumber(b.id)
        if na and nb then return na < nb end
        return tostring(a.id) < tostring(b.id)
    end)
    return ignored
end

local function buildIgnoredPanel(panel)
    if panel.ignoredFrame then return end
    local f = CreateFrame("Frame", nil, panel, "BackdropTemplate")
    f:SetWidth(360)
    f:SetPoint("TOPLEFT", panel, "TOPRIGHT", 10, 0)
    f:SetPoint("BOTTOMLEFT", panel, "BOTTOMRIGHT", 10, 0)
    f:SetFrameStrata(panel:GetFrameStrata() or "MEDIUM")
    f:SetFrameLevel((panel:GetFrameLevel() or 10) + 20)
    f:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 8, edgeSize = 12,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    f:SetBackdropColor(0.05, 0.05, 0.05, 0.95)
    f:SetBackdropBorderColor(0.2, 0.2, 0.2, 1)
    f:Hide()

    local scroll = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -32)
    scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -26, 8)

    local child = CreateFrame("Frame", nil, scroll)
    child:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0)
    child:SetPoint("RIGHT", scroll, "RIGHT", 0, 0)
    child:SetWidth(240)
    scroll:SetScrollChild(child)
    scroll:HookScript("OnSizeChanged", function(self, w)
        if child and w and w > 0 then
            child:SetWidth(w - 2)
        end
    end)
    initLegacyScrollFrame(scroll, RESULT_ROW_EXTENT)
    local overlay = child:CreateTexture(nil, "BACKGROUND")
    overlay:SetAllPoints()
    overlay:SetColorTexture(0, 0, 0, 0.3)
    child._overlay = overlay

    local header = f:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    header:SetPoint("TOP", f, "TOP", 0, -12)
    header:SetJustifyH("CENTER")
    header:SetText("Ignored Achievements")

    panel.ignoredFrame = f
    panel.ignoredScroll = scroll
    panel.ignoredChild = child

    if ns.Panel and ns.Panel.ApplyTheme then
        ns.Panel:ApplyTheme()
    end
end

local function setPanelButtonActive(panel, key)
    if not panel then return end
    local map = {
        filters = panel.filtersButton,
        meta = panel.metaButton,
        ignored = panel.ignoredButton,
    }
    for name, btn in pairs(map) do
        if btn then
            if key and name == key then
                if btn.SetButtonState then
                    btn:SetButtonState("PUSHED", true)
                end
                if btn.LockHighlight then
                    btn:LockHighlight()
                end
            else
                if btn.SetButtonState then
                    btn:SetButtonState("NORMAL", false)
                end
                if btn.UnlockHighlight then
                    btn:UnlockHighlight()
                end
            end
        end
    end
end

function Panel:RefreshPanelButtons()
    if not self.frame then return end
    if self.frame.ignoredFrame and self.frame.ignoredFrame:IsShown() then
        setPanelButtonActive(self.frame, "ignored")
    elseif self.frame.metaFrame and self.frame.metaFrame:IsShown() then
        setPanelButtonActive(self.frame, "meta")
    elseif self.frame.filtersFrame and self.frame.filtersFrame:IsShown() then
        setPanelButtonActive(self.frame, "filters")
    else
        setPanelButtonActive(self.frame, nil)
    end
end

function Panel:ShowIgnoredPanel()
    if not self.frame then return end
    buildIgnoredPanel(self.frame)
    local f = self.frame.ignoredFrame
    if not f then return end
    if self.frame.filtersFrame and self.frame.filtersFrame:IsShown() then
        self.frame.filtersFrame:Hide()
    end
    if self.frame.metaFrame and self.frame.metaFrame:IsShown() then
        self.frame.metaFrame:Hide()
    end
    if not f:IsShown() then
        f:Show()
        self:RenderIgnored()
    end
    setPanelButtonActive(self.frame, "ignored")
    if self.SaveViewState then self:SaveViewState() end
    self:RefreshPanelButtons()
end

function Panel:ToggleIgnored()
    if not self.frame then return end
    buildIgnoredPanel(self.frame)
    local f = self.frame.ignoredFrame
    if not f then return end
    if self.frame.filtersFrame and self.frame.filtersFrame:IsShown() then
        self.frame.filtersFrame:Hide()
    end
    if self.frame.metaFrame and self.frame.metaFrame:IsShown() then
        self.frame.metaFrame:Hide()
    end
    if f:IsShown() then
        f:Hide()
    else
        f:Show()
        self:RenderIgnored()
    end
    if self.SaveViewState then self:SaveViewState() end
    self:RefreshPanelButtons()
end

local function buildTabs(panel)
end

local function applyCollapsedState(panel, collapsed, instant)
end

function Panel:SaveViewState()
    if not ACAU_Config then return end
    ACAU_Config.viewState = ACAU_Config.viewState or {}
    local vs = ACAU_Config.viewState
    vs.filtersOpen = self.frame and self.frame.filtersFrame and self.frame.filtersFrame:IsShown() or false
    vs.ignoredOpen = self.frame and self.frame.ignoredFrame and self.frame.ignoredFrame:IsShown() or false
    vs.metaOpen = self.frame and self.frame.metaFrame and self.frame.metaFrame:IsShown() or false
    vs.metaMode = self._metaMode or "landing"
    vs.metaSelectedID = self._metaSelectedID
end

function Panel:RestoreViewState()
    if not self.frame or not ACAU_Config then return end
    local vs = ACAU_Config.viewState or {}
    if vs.filtersOpen then
        buildFiltersPanel(self.frame)
        if self.frame.filtersFrame then self.frame.filtersFrame:Show() end
    end
    if vs.ignoredOpen then
        buildIgnoredPanel(self.frame)
        if self.frame.ignoredFrame then
            self.frame.ignoredFrame:Show()
            self:RenderIgnored()
        end
    end
    if vs.metaOpen and Util:IsRetail() then
        buildMetaPanel(self.frame)
        if self.frame.metaFrame then
            self.frame.metaFrame:Show()
            if vs.metaMode == "detail" and vs.metaSelectedID then
                self._metaMode = "detail"
                self._metaSelectedID = vs.metaSelectedID
                self:RenderMetaDetail(vs.metaSelectedID)
            else
                self._metaMode = "landing"
                self._metaSelectedID = nil
                self:RenderMetaLanding()
            end
        end
    end
    self:RefreshPanelButtons()
end

local DEFAULT_ANCHOR_POINT = { "TOPLEFT", "AchievementFrame", "TOPRIGHT", 24, -12 }
function Panel:RefreshTop5Override(baseList)
    local list = baseList or (ns.State and ns.State.results) or {}
    local overrideID = ACAU_Config and ACAU_Config.metaTopOverride
    local overrideName = ACAU_Config and ACAU_Config.metaTopOverrideName
    if overrideID and Meta and Meta.GetMetaDetail then
        local data = Meta:GetMetaDetail(overrideID) or {}
        if not overrideName then
            local metas = Meta:GetMetas() or {}
            for _, metasByExp in pairs(metas) do
                for _, m in ipairs(metasByExp) do
                    if m.id == overrideID then
                        overrideName = m.name
                        ACAU_Config.metaTopOverrideName = overrideName
                        break
                    end
                end
            end
        end
        local subset = {}
        for i = 1, math.min(5, #data) do
            table.insert(subset, data[i])
        end
        if #subset > 0 then
            list = subset
        end
    end
    if ns.Tracker and ns.Tracker.Update then
        ns.Tracker:Update(list)
        if ns.Tracker.SetHeader then
            local label = (overrideID and overrideName) and (overrideName .. " Top 5") or "ACA: Top 5"
            ns.Tracker:SetHeader(label)
        end
    end
end

local function getDefaultPanelPoint()
    return { DEFAULT_ANCHOR_POINT[1], DEFAULT_ANCHOR_POINT[2], DEFAULT_ANCHOR_POINT[3], DEFAULT_ANCHOR_POINT[4], DEFAULT_ANCHOR_POINT[5] }
end

local function resolveAnchorParent(parentRef)
    local parent = parentRef
    if type(parent) == "string" then
        parent = _G[parent]
        if not parent and parentRef == "AchievementFrame" then
            ensureAchievementUI(false)
            parent = _G[parentRef]
        end
    end
    if not (parent and parent.GetObjectType and parent:GetObjectType()) then
        parent = UIParent
    end
    return parent
end

local function getPanelPointStore()
    if ACAU_Global and ACAU_Global.positionAccountWide then
        return ACAU_Global
    end
    return ACAU_Config
end

local function applyAnchor(panel)
    if not panel then return end
    local store = getPanelPointStore()
    local p = (store and store.panelPoint) or getDefaultPanelPoint()
    if store and not store.panelPoint then
        store.panelPoint = { p[1], p[2], p[3], p[4], p[5] }
    end
    local parent = resolveAnchorParent(p[2])
    panel._anchorParent = parent
    panel:ClearAllPoints()
    panel:SetPoint(p[1] or "TOPLEFT", parent, p[3] or p[1] or "TOPLEFT", p[4] or 0, p[5] or 0)
end

function Panel:ResetPosition()
    local store = getPanelPointStore()
    if not store then return end
    local p = getDefaultPanelPoint()
    store.panelPoint = { p[1], p[2], p[3], p[4], p[5] }
    if self.frame then
        applyAnchor(self.frame)
    end
end

function Panel:UpdateRemixUI()
    if not self.frame then return end
    local active = ACAU_Config and ACAU_Config.remixModeEnabled == true
    local palette = getPalette((ACAU_Config and ACAU_Config.theme) or Const.DEFAULTS.theme)
    local accent = palette and palette.accent or { 1, 0.78, 0.2 }
    if self.frame.remixBadge then
        if active then
            self.frame.remixBadge:SetText("Remix Mode")
            self.frame.remixBadge:SetTextColor(accent[1] or 0.8, accent[2] or 1, accent[3] or 0.3)
        else
            self.frame.remixBadge:SetText("")
        end
    end
end

function Panel:ApplyTheme()
    local themeKey = normalizeThemeKey((ACAU_Config and ACAU_Config.theme) or Const.DEFAULTS.theme or "Modern")
    local style = getThemeStyle(themeKey)
    local palette = getPalette(themeKey)
    applyBackdrop(self.frame, style, themeKey)
    applyBackdrop(self.frame and self.frame._inset, style, themeKey)
    applyBackdrop(self.frame and self.frame.filtersFrame, style, themeKey)
    applyBackdrop(self.frame and self.frame.ignoredFrame, style, themeKey)
    applyBackdrop(self.frame and self.frame.metaFrame, style, themeKey)
    local function tintButtons(btns)
        for _, btn in ipairs(btns) do
            if btn then
                local fs = btn.Text or (btn.GetFontString and btn:GetFontString())
                if fs and palette and palette.buttonText then
                    fs:SetTextColor(unpack(palette.buttonText))
                elseif fs and palette and palette.accent then
                    fs:SetTextColor(unpack(palette.accent))
                end
                local hi = btn.GetHighlightTexture and btn:GetHighlightTexture()
                if hi then
                    local hr, hg, hb, ha = 1, 1, 1, 0.25
                    if palette and palette.highlight then
                        hr, hg, hb, ha = palette.highlight[1], palette.highlight[2], palette.highlight[3], palette.highlight[4] or ha
                    elseif palette and palette.accent then
                        hr, hg, hb = palette.accent[1], palette.accent[2], palette.accent[3]
                    end
                    hi:SetVertexColor(hr or 1, hg or 1, hb or 1, ha or 0.25)
                    hi:SetBlendMode("ADD")
                end
            end
        end
    end
    tintButtons({ self.frame and self.frame.topBtn, self.frame and self.frame.filtersButton, self.frame and self.frame.metaButton, self.frame and self.frame.ignoredButton, self.frame and self.frame.rescanBtn })
    if self.frame and self.frame.progress and palette and palette.accent then
        local tex = self.frame.progress:GetStatusBarTexture()
        if tex then tex:SetVertexColor(palette.accent[1], palette.accent[2], palette.accent[3], 1) end
        self.frame.progress.bg:SetColorTexture(0, 0, 0, 0.85)
        if self.frame.progress.Text and palette.textPrimary then
            self.frame.progress.Text:SetTextColor(palette.textPrimary[1], palette.textPrimary[2], palette.textPrimary[3])
        end
    end
    if self.frame and self.frame.filtersFrame then
        if self.frame.filtersFrame:GetBackdrop() then applyBackdrop(self.frame.filtersFrame, style, themeKey) end
        local child = self.frame.filtersFrame:GetChildren()
        if child and child._overlay then
            if themeKey == "Parchment" then child._overlay:Show() else child._overlay:Hide() end
        end
        local primary = palette and palette.textPrimary or { 1, 0.82, 0 }
        local secondary = palette and palette.textSecondary or primary
        tintFontsRecursive(self.frame.filtersFrame, primary[1], primary[2], primary[3])
        tintCheckboxLabels(self.frame.filtersFrame, secondary[1], secondary[2], secondary[3])
    end
    if self.frame and self.frame.ignoredFrame then
        if self.frame.ignoredFrame:GetBackdrop() then applyBackdrop(self.frame.ignoredFrame, style, themeKey) end
        local child = self.frame.ignoredFrame:GetChildren()
        if child and child._overlay then
            if themeKey == "Parchment" then child._overlay:Show() else child._overlay:Hide() end
        end
        local primary = palette and palette.textPrimary or { 1, 0.82, 0 }
        tintFontsRecursive(self.frame.ignoredFrame, primary[1], primary[2], primary[3])
    end
    if self.frame and self.frame.metaFrame then
        if self.frame.metaFrame:GetBackdrop() then applyBackdrop(self.frame.metaFrame, style, themeKey) end
        local child = self.frame.metaFrame:GetChildren()
        if child and child._overlay then
            if themeKey == "Parchment" then child._overlay:Show() else child._overlay:Hide() end
        end
        local primary = palette and palette.textPrimary or { 1, 0.82, 0 }
        tintFontsRecursive(self.frame.metaFrame, primary[1], primary[2], primary[3])
    end
    if self.linkPopup then
        local baseStrata = (self.frame and self.frame.GetFrameStrata and self.frame:GetFrameStrata()) or "MEDIUM"
        self.linkPopup:SetFrameStrata(getHigherStrata(baseStrata))
        applyBackdrop(self.linkPopup, style, themeKey)
        local primary = palette and palette.textPrimary or { 1, 0.82, 0 }
        if self.linkPopup.edit and self.linkPopup.edit.SetTextColor then
            self.linkPopup.edit:SetTextColor(primary[1], primary[2], primary[3])
        end
        if self.linkPopup.title and self.linkPopup.title.SetTextColor then
            self.linkPopup.title:SetTextColor(primary[1], primary[2], primary[3])
        end
        if self.linkPopup.copyHint and self.linkPopup.copyHint.SetTextColor and palette and palette.textSecondary then
            self.linkPopup.copyHint:SetTextColor(palette.textSecondary[1], palette.textSecondary[2], palette.textSecondary[3])
        end
    end
    self:UpdateRemixUI()
    if ns.Tracker and ns.Tracker.ApplyTheme then
        ns.Tracker:ApplyTheme()
    end
end

function Panel:GetThemeStyle(themeKey)
    return getThemeStyle(normalizeThemeKey(themeKey or (ACAU_Config and ACAU_Config.theme) or Const.DEFAULTS.theme))
end

function Panel:RefreshTheme()
    if not self.frame then return end
    self:ApplyTheme()
    if ns.State and ns.State.results then
        self:Render(ns.State.results)
    end
    if self.frame.ignoredFrame and self.frame.ignoredFrame:IsShown() then
        self:RenderIgnored()
    end
    if self.frame.metaFrame and self.frame.metaFrame:IsShown() then
        if self._metaMode == "detail" then
            self:RenderMetaDetail(self._metaSelectedID)
        else
            self:RenderMetaLanding()
        end
    end
end

function Panel:SetDetached(detached)
    ACAU_Config.detached = not not detached
    applyAnchor(self.frame)
end

function Panel:RefreshAnchor()
    applyAnchor(self.frame)
end

local function setupHeader(panel, rescan)
    local icon = panel:CreateTexture(nil, "ARTWORK")
    icon:SetSize(24, 24)
    icon:SetPoint("TOPLEFT", panel, "TOPLEFT", 12, -8)
    icon:SetTexture(ICON)

    local title = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("LEFT", icon, "RIGHT", 6, 0)
    title:SetText("Almost Completed Achievements")

    local remixBadge = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    remixBadge:SetPoint("LEFT", title, "RIGHT", 8, 0)
    panel.remixBadge = remixBadge
end

local function hookAchievementFrame()
    if Panel._achFrameHooked then return end
    ensureAchievementUI(false)
    if not AchievementFrame or not AchievementFrame.HookScript then return end
    AchievementFrame:HookScript("OnShow", function()
        if ns.Panel and ns.Panel.OnAchievementFrameShow then
            ns.Panel:OnAchievementFrameShow()
        end
    end)
    AchievementFrame:HookScript("OnHide", function()
        if ns.Panel and ns.Panel.OnAchievementFrameHide then
            ns.Panel:OnAchievementFrameHide()
        end
    end)
    Panel._achFrameHooked = true
end

local function setupControls(panel, rescan)
    local function applyFilters()
        if ns.ApplyFilters then ns.ApplyFilters() end
    end

    local filterLabel = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    filterLabel:SetPoint("TOPLEFT", panel, "TOPLEFT", 16, -36)
    filterLabel:SetText("Reward")

    local filterDrop = createDropdown(panel, 180, ACAU_Config.filterMode or "All", Const.FILTER_LIST, function(name)
        ACAU_Config.filterMode = name
        applyFilters()
    end)
    filterDrop:SetPoint("TOPLEFT", filterLabel, "BOTTOMLEFT", -2, -2)

    local sortLabel = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    sortLabel:SetPoint("TOPLEFT", filterLabel, "TOPRIGHT", 200, 0)
    sortLabel:SetText("Sort by")

    local sortDrop = createDropdown(panel, 160, ACAU_Config.sortMode or "Percent Desc.", Const.SORT_LIST, function(mode)
        ACAU_Config.sortMode = mode
        applyFilters()
    end)
    sortDrop:SetPoint("TOPLEFT", sortLabel, "BOTTOMLEFT", -2, -2)
end

function Panel:Init(rescan)
    if self.frame then return end
    local panel = CreateFrame("Frame", Const.PANEL_NAME, UIParent, "BackdropTemplate")
    panel:SetSize(460, 520)
    local themeKey = normalizeThemeKey((ACAU_Config and ACAU_Config.theme) or Const.DEFAULTS.theme)
    applyBackdrop(panel, getThemeStyle(themeKey), themeKey)
    panel:SetMovable(true)
    panel:EnableMouse(true)
    panel:RegisterForDrag("LeftButton")
    panel:SetScript("OnDragStart", function(self)
        if ACAU_Config.detached then self:StartMoving() end
    end)
    panel:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local a1, parent, a2, x, y = self:GetPoint()
        local parentName = (parent and parent.GetName and parent:GetName()) or "UIParent"
        local store = getPanelPointStore()
        if store then
            store.panelPoint = { a1 or "CENTER", parentName, a2 or a1 or "CENTER", x or 0, y or 0 }
        end
    end)
    panel:Hide()
    self.frame = panel
    registerForEscapeClose(panel)

    local inset = CreateFrame("Frame", nil, panel, "BackdropTemplate")
    inset:SetPoint("TOPLEFT", panel, "TOPLEFT", 8, -80)
    inset:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -8, 74)
    applyBackdrop(inset, getThemeStyle(themeKey), themeKey)
    panel._inset = inset

    setupHeader(panel, rescan)
    setupControls(panel, rescan)

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

    local helpBtn = CreateFrame("Button", nil, panel)
    helpBtn:SetSize(22, 22)
    helpBtn:SetPoint("RIGHT", closeBtn, "LEFT", -2, 0)
    local qTex = helpBtn:CreateTexture(nil, "ARTWORK")
    qTex:SetAllPoints()
    qTex:SetTexture("Interface\\GossipFrame\\ActiveQuestIcon")
    helpBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_LEFT")
        GameTooltip:AddLine("How to use ACA", 1, 0.82, 0)
        GameTooltip:AddLine("Set filters and threshold, then Rescan to refresh results.", 0.9, 0.9, 0.9, true)
        GameTooltip:AddLine("After earning achievements, ACA removes them from the list automatically, but a Rescan is recommended.", 0.9, 0.9, 0.9, true)
        GameTooltip:AddLine("Left-click: open achievement in Blizzard UI.", 0.9, 0.9, 0.9, true)
        GameTooltip:AddLine("Shift+Left-click: track/untrack in Blizzard tracker.", 0.9, 0.9, 0.9, true)
        GameTooltip:AddLine("Right-click: copy Wowhead link.", 0.9, 0.9, 0.9, true)
        GameTooltip:AddLine("Options: Esc > Options > AddOns > ACA.", 0.9, 0.9, 0.9, true)
        GameTooltip:AddLine("Slash: /aca to open this window.", 0.9, 0.9, 0.9, true)
        GameTooltip:Show()
    end)
    helpBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    if type(ns.EnsureDevInfoModule) == "function" then
        local devInfoModule = ns.EnsureDevInfoModule()
        if devInfoModule and devInfoModule.AttachButton then
            panel.devInfoButton = devInfoModule:AttachButton(panel, {
                point = "RIGHT",
                relative_to = helpBtn,
                relative_point = "LEFT",
                x = -4,
                y = 0,
                width = 58,
                height = 20,
                frame_level_offset = 12,
                text = "Dev Info",
            })
        end
    end

    applyAnchor(panel)
    panel._anim = panel:CreateAnimationGroup()
    panel._anim.translation = panel._anim:CreateAnimation("Translation")
    panel._anim.translation:SetDuration(0.18)
    panel._anim:SetScript("OnPlay", function(anim)
        local p = anim:GetParent()
        p._startOffset = p._currentOffset or p._baseOffset or 0
    end)
    panel._anim:SetScript("OnFinished", function(anim)
        local p = anim:GetParent()
        if p._anim.target then
            p._currentOffset = p._anim.target
            applyCollapsedState(p, ACAU_Config.collapsed, true)
        end
    end)
    panel._anim:SetScript("OnUpdate", function(anim, elapsed)
        local p = anim:GetParent()
        local target = p._anim.target or p._currentOffset or 0
        local start = p._startOffset or target
        local progress = anim:GetProgress()
        local val = start + ((target - start) * progress)
        local side = ACAU_Config.anchorSide == "LEFT" and "TOPRIGHT" or "TOPLEFT"
        local rel = ACAU_Config.anchorSide == "LEFT" and "TOPLEFT" or "TOPRIGHT"
        p:ClearAllPoints()
        p:SetPoint(side, p._anchorParent, rel, val, 0)
    end)

    local scroll1 = CreateFrame("ScrollFrame", panel:GetName() .. "Scroll1", panel._inset, "UIPanelScrollFrameTemplate")
    scroll1:SetPoint("TOPLEFT", panel._inset, "TOPLEFT", 8, -8)
    scroll1:SetPoint("BOTTOMRIGHT", panel._inset, "BOTTOMRIGHT", -26, 8)
    local child1 = CreateFrame("Frame", nil, scroll1)
    child1:SetPoint("TOPLEFT", scroll1, "TOPLEFT", 0, 0)
    child1:SetPoint("RIGHT", scroll1, "RIGHT", -4, 0)
    child1:SetHeight(600)
    scroll1:SetScrollChild(child1)
    scroll1:HookScript("OnSizeChanged", function(self, w)
        if child1 then child1:SetWidth(w - 2) end
    end)
    initLegacyScrollFrame(scroll1, RESULT_ROW_EXTENT)

    panel.scrollCompleted = scroll1
    panel.childCompleted = child1

    local progress = CreateFrame("StatusBar", nil, panel, "TextStatusBar")
    progress:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 16, 22)
    progress:SetPoint("RIGHT", panel, "RIGHT", -16, 22)
    progress:SetHeight(18)
    progress:SetStatusBarTexture("Interface\\TARGETINGFRAME\\UI-StatusBar")
    progress:GetStatusBarTexture():SetHorizTile(false)
    progress:GetStatusBarTexture():SetVertexColor(0, 0.8, 0.2, 1)
    progress:SetMinMaxValues(0, 1)
    progress:SetValue(0)
    progress.bg = progress:CreateTexture(nil, "BACKGROUND")
    progress.bg:SetAllPoints(progress)
    progress.bg:SetColorTexture(0.05, 0.05, 0.05, 0.9)
    progress.Text = progress:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    progress.Text:SetPoint("CENTER", progress, "CENTER", 0, 0)
    progress.Text:SetText(ACAU_Global and ACAU_Global.manualScanOnly and "Scan on login disabled; press Rescan" or "Idle")
    panel.progress = progress

    local btnRowY = 46
    local topBtn = createMainPanelButton(panel, "Top 5", function()
        if ns.Tracker and ns.Tracker.Toggle then ns.Tracker:Toggle() end
    end)
    panel.topBtn = topBtn

    local filtersBtn = createMainActionFilterButton(panel)
    panel.filtersButton = filtersBtn

    local metaBtn
    if Util:IsRetail() then
        metaBtn = createMainPanelButton(panel, "Meta", function()
            if ns.Panel and ns.Panel.ToggleMeta then
                ns.Panel:ToggleMeta()
            end
        end)
        panel.metaButton = metaBtn
    end

    local ignoredBtn = createMainPanelButton(panel, "Ignored", function()
        if ns.Panel and ns.Panel.ToggleIgnored then
            ns.Panel:ToggleIgnored()
        end
    end)
    panel.ignoredButton = ignoredBtn

    local rescanBtn = createMainPanelButton(panel, "Rescan", function() rescan(true) end)
    panel.rescanBtn = rescanBtn

    local function layoutButtons()
        if not (panel.topBtn and panel.filtersButton and panel.ignoredButton and panel.rescanBtn) then return end
        local margin = 16
        local spacing = 8
        local btns = { panel.topBtn, panel.filtersButton, panel.metaButton, panel.ignoredButton, panel.rescanBtn }
        local actual = {}
        for _, b in ipairs(btns) do if b then table.insert(actual, b) end end
        local count = #actual
        local width = (panel:GetWidth() or 460) - (margin * 2) - (spacing * (count - 1))
        local btnW = math.max(80, width / count)
        local btnH = 24

        local anchor = panel.topBtn
        anchor:ClearAllPoints()
        anchor:SetSize(btnW, btnH)
        anchor:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", margin, btnRowY)

        local prev = anchor
        for idx = 2, count do
            local btn = actual[idx]
            btn:ClearAllPoints()
            btn:SetSize(btnW, btnH)
            btn:SetPoint("LEFT", prev, "RIGHT", spacing, 0)
            prev = btn
        end
    end

    layoutButtons()
    self:UpdateRemixUI()
    panel:SetScript("OnSizeChanged", layoutButtons)

    panel:SetScript("OnHide", function()
        ACAU_Config.panelHidden = true
        if ns.Panel and ns.Panel.SaveViewState then ns.Panel:SaveViewState() end
    end)
    panel:SetScript("OnShow", function()
        ACAU_Config.panelHidden = false
        if ns.Panel and ns.Panel.RestoreViewState then ns.Panel:RestoreViewState() end
    end)

    applyCollapsedState(panel, ACAU_Config.collapsed, true)
    hookAchievementFrame()
    self:RestoreViewState()
    self:ApplyTheme()
end

function Panel:Toggle()
    if not self.frame then return end
    if self.frame:IsShown() then
        self.frame:Hide()
    else
        applyAnchor(self.frame)
        self.frame:Show()
    end
end

function Panel:OnAchievementFrameShow()
    if not (ACAU_Global and ACAU_Global.toggleWithAchievementFrame) then return end
    if not self.frame then return end
    self._closeWithAchievement = true
    applyAnchor(self.frame)
    self.frame:Show()
end

function Panel:OnAchievementFrameHide()
    if not (ACAU_Global and ACAU_Global.toggleWithAchievementFrame) then return end
    if not self.frame then return end
    if self._closeWithAchievement and self.frame:IsShown() then
        self.frame:Hide()
    end
    self._closeWithAchievement = false
end

function Panel:OnProgress(done, total)
    if not self.frame or not self.frame.progress then return end
    local bar = self.frame.progress
    bar:SetMinMaxValues(0, total > 0 and total or 1)
    bar:SetValue(done)
    bar.Text:SetText(("Scanning %d / %d"):format(done, total))
end

function Panel:Render(list)
    if not self.frame then return end
    self:ApplyTheme()
    local parent = self.frame.childCompleted
    local rows = parent._rows or {}
    for _, r in ipairs(rows) do r:Hide() end
    parent._rows = rows
    local themeKey = (ACAU_Config and ACAU_Config.theme) or Const.DEFAULTS.theme
    local palette = getPalette(themeKey)
    local nameR, nameG, nameB = 1, 0.82, 0
    local detailR, detailG, detailB = 0.9, 0.9, 0.9
    local accentR, accentG, accentB = 1, 0.78, 0.2
    local accent2R, accent2G, accent2B = 0.55, 0.78, 1
    local highlightR, highlightG, highlightB, highlightA = 1, 1, 1, 0.25
    local dividerA = 0.12
    if palette then
        if palette.textPrimary then nameR, nameG, nameB = unpack(palette.textPrimary) end
        if palette.textSecondary then detailR, detailG, detailB = unpack(palette.textSecondary) end
        if palette.accent then accentR, accentG, accentB = unpack(palette.accent) end
        if palette.accentAlt then accent2R, accent2G, accent2B = unpack(palette.accentAlt) end
        if palette.highlight then
            highlightR, highlightG, highlightB, highlightA = palette.highlight[1], palette.highlight[2], palette.highlight[3], palette.highlight[4] or highlightA
        end
        if palette.dividerAlpha then dividerA = palette.dividerAlpha end
    end

    local total = list and #list or 0
    local y = -4
    for i, ach in ipairs(list or {}) do
        local row = rows[i] or makeRow(parent)
        rows[i] = row
        row:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, y)
        row:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -4, y)
        if row.bg then
            local evenA = palette and palette.rowEvenAlpha or 0.32
            local oddA = palette and palette.rowOddAlpha or 0.22
            if i % 2 == 0 then
                row.bg:SetColorTexture(0, 0, 0, evenA)
            else
                row.bg:SetColorTexture(0, 0, 0, oddA)
            end
        end
        row.Icon:SetTexture(ach.icon or 134400)
        row.Name:SetText(ach.name or ("[" .. tostring(ach.id) .. "]"))
        row.Percent:SetText(("%.0f%%"):format(ach.percent or 0))
        row.Reward:SetText(Util:Truncate(ach.reward, 28))
        row.Points:SetText(ach.points and ach.points > 0 and (ach.points .. " pts") or "")
        row._achID = ach.id
        row._achName = ach.name
        row._achData = ach
        row._reward = ach.reward
        if row.Divider then
            if i < total then
                row.Divider:SetColorTexture(1, 1, 1, dividerA)
                row.Divider:Show()
            else
                row.Divider:Hide()
            end
        end
        if row.Name then row.Name:SetTextColor(nameR, nameG, nameB) end
        if row.Percent then row.Percent:SetTextColor(accentR, accentG, accentB) end
        if row.Reward then row.Reward:SetTextColor(detailR, detailG, detailB) end
        if row.Points then row.Points:SetTextColor(accent2R, accent2G, accent2B) end
        if row.highlight then
            row.highlight:SetVertexColor(highlightR, highlightG, highlightB, highlightA)
        end
        tooltipForRow(row, ach.id, ach.reward)
        if not row._acaMainBound then
            row:SetScript("OnClick", onAchievementRowClick)
            row._acaMainBound = true
        end
        applyRowIgnoreResetStyle(row.Ignore)
        row.Ignore:SetPoint("TOPRIGHT", row, "TOPRIGHT", -8, -6)
        row.Ignore._acaTooltipText = "Ignore this achievement"
        row.Ignore:SetScript("OnEnter", onActionButtonTooltipEnter)
        row.Ignore:SetScript("OnLeave", onActionButtonTooltipLeave)
        row.Ignore:SetScript("OnClick", onResultIgnoreClick)
        row.IgnoreAll:Hide()
        row:Show()
        y = y - 52
    end
    parent:SetHeight(math.abs(y))
    if self.frame.progress then
        local count = list and #list or 0
        self.frame.progress:SetValue(0)
        self.frame.progress.Text:SetText(("Found %d results"):format(count))
    end

    if total == 0 then
        if not parent._emptyText then
            local t = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
            t:SetPoint("TOP", parent, "TOP", 0, -8)
            t:SetText("No results. Adjust filters/threshold and Rescan.")
            t:SetJustifyH("CENTER")
            parent._emptyText = t
        end
        parent._emptyText:Show()
    elseif parent._emptyText then
        parent._emptyText:Hide()
    end
end

function Panel:RenderIgnored()
    if not (self.frame and self.frame.ignoredChild) then return end
    self:ApplyTheme()
    local parent = self.frame.ignoredChild
    local rows = parent._rows or {}
    for _, r in ipairs(rows) do r:Hide() end
    parent._rows = rows
    local themeKey = (ACAU_Config and ACAU_Config.theme) or Const.DEFAULTS.theme
    local palette = getPalette(themeKey)
    local nameR, nameG, nameB = 1, 0.82, 0
    local detailR, detailG, detailB = 0.9, 0.9, 0.9
    local dividerA = 0.12
    if palette then
        if palette.textPrimary then nameR, nameG, nameB = unpack(palette.textPrimary) end
        if palette.textSecondary then detailR, detailG, detailB = unpack(palette.textSecondary) end
        if palette.dividerAlpha then dividerA = palette.dividerAlpha end
    end

    local ignored = collectIgnoredEntries()

    local total = #ignored
    if parent._overlay then
        if total == 0 then parent._overlay:Hide() else parent._overlay:Show() end
    end
    local y = -4
    local missing = 0
    for i, entry in ipairs(ignored) do
        local achID = entry.id
        local achKey = achID
        local achNumeric = tonumber(achID) or achID
        local name, icon = storedIgnoredInfo(achKey)
        if (not name or name == "") or not icon then
            local fetchedName, fetchedIcon = getNameIconWithFallback(achNumeric)
            name = (name and name ~= "" and name) or fetchedName
            icon = icon or fetchedIcon
            if name and name ~= "" or icon then
                rememberIgnoredInfo(achKey, name, icon)
            end
        end
        if (not name or name == "") then
            pendingAchievementNames[achID] = true
            if C_AchievementInfo and C_AchievementInfo.RequestAchievementInfo then
                C_AchievementInfo.RequestAchievementInfo(achNumeric)
            end
            missing = missing + 1
        end
        local row = rows[i] or makeRow(parent)
        rows[i] = row
        row:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, y)
        row:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -4, y)
        if row.bg then
            local evenA = palette and palette.rowEvenAlpha or 0.32
            local oddA = palette and palette.rowOddAlpha or 0.22
            if i % 2 == 0 then
                row.bg:SetColorTexture(0, 0, 0, evenA)
            else
                row.bg:SetColorTexture(0, 0, 0, oddA)
            end
        end
        row.Icon:SetTexture(icon or 134400)
        row.Name:SetText(name or ("[" .. achID .. "]"))
        if entry.scope == "global" or entry.scope == "both" then
            row.Percent:SetText("Ignored on all characters")
        else
            row.Percent:SetText("Ignored")
        end
        row.Reward:SetText("")
        row.Points:SetText("")
        row._achID = achNumeric
        row._achName = name
        row._achIDKey = achID
        row._achStoredName = name
        row._achStoredIcon = icon
        row._achData = nil
        if row.Name then row.Name:SetTextColor(nameR, nameG, nameB) end
        if row.Percent then row.Percent:SetTextColor(detailR, detailG, detailB) end
        row._reward = nil
        if row.Divider then
            if i < total then
                row.Divider:SetColorTexture(1, 1, 1, dividerA)
                row.Divider:Show()
            else
                row.Divider:Hide()
            end
        end
        tooltipForRow(row, achNumeric)
        if not row._acaMainBound then
            row:SetScript("OnClick", onAchievementRowClick)
            row._acaMainBound = true
        end
        applyRowRestoreExpandStyle(row.Ignore)
        row.Ignore:SetPoint("TOPRIGHT", row, "TOPRIGHT", -8, -26)
        row.Ignore._acaTooltipText = "Restore this achievement"
        row.Ignore:SetScript("OnEnter", onActionButtonTooltipEnter)
        row.Ignore:SetScript("OnLeave", onActionButtonTooltipLeave)
        row.Ignore:SetScript("OnClick", onIgnoredRestoreClick)
        applyRowIgnoreAllExitStyle(row.IgnoreAll)
        row.IgnoreAll:SetSize(20, 20)
        row.IgnoreAll:SetPoint("TOPRIGHT", row, "TOPRIGHT", -8, -6)
        if entry.scope == "global" or entry.scope == "both" then
            row.IgnoreAll:Hide()
        else
            row.IgnoreAll:Show()
            row.IgnoreAll._acaTooltipText = "Ignore on all characters"
            row.IgnoreAll:SetScript("OnEnter", onActionButtonTooltipEnter)
            row.IgnoreAll:SetScript("OnLeave", onActionButtonTooltipLeave)
            row.IgnoreAll:SetScript("OnClick", onIgnoredIgnoreAllClick)
        end
        row:Show()
        y = y - 52
    end
    parent:SetHeight(math.abs(y))

    if missing > 0 and not pendingIgnoredTimer then
        pendingIgnoredTimer = C_Timer.After(0.5, function()
            pendingIgnoredTimer = nil
            if not (ns.Panel and ns.Panel.frame and ns.Panel.frame.ignoredFrame and ns.Panel.frame.ignoredFrame:IsShown()) then
                return
            end
            wipe(pendingAchievementNames)
            ns.Panel:RenderIgnored()
        end)
    end
end
