-- rMinimap: GUI  (nBuff-style, single-page)
local addonName, ns = ...
local L = ns.L or {}
local function T(key, fallback)
    local v = L[key]
    if v == nil or v == "" then return fallback or key end
    return v
end

local C = {
    primary    = {0.965, 0.533, 0.000},
    primaryHi  = {1.000, 0.700, 0.250},
    bg         = {0.060, 0.060, 0.060, 0.98},
    panel      = {0.105, 0.105, 0.105, 0.95},
    border     = {0.20,  0.20,  0.20,  1.0},
    text       = {0.90,  0.90,  0.90},
    textDim    = {0.50,  0.50,  0.50},
    textBright = {1.00,  1.00,  1.00},
    separator  = {0.25,  0.25,  0.25,  0.55},
}

local FRAME_W  = 840
local FRAME_H  = 660
local TITLE_H  = 50
local SIDEBAR_W = 180
local BOT_H    = 40
local PAD      = 18
local CONTENT_W = FRAME_W - SIDEBAR_W - 8

-- ── helpers ───────────────────────────────────────────────────────────────
local function Tex(parent, layer) return parent:CreateTexture(nil, layer or "ARTWORK") end
local function FS(parent, template) return parent:CreateFontString(nil, "OVERLAY", template or "GameFontNormal") end

local function FlatFrame(parent, r, g, b, a, br, bg_, bb, ba)
    local f = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    f:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8", edgeFile="Interface\\Buttons\\WHITE8X8",
        tile=false, edgeSize=1, insets={left=0,right=0,top=0,bottom=0}})
    f:SetBackdropColor(r or 0, g or 0, b or 0, a or 1)
    f:SetBackdropBorderColor(br or r or 0, bg_ or g or 0, bb or b or 0, ba or a or 1)
    return f
end

local function SectionHeader(parent, text, anchor, xOfs, yOfs)
    local lbl = FS(parent, "GameFontNormal")
    lbl:SetPoint("TOPLEFT", anchor or parent, "TOPLEFT", xOfs or 0, yOfs or 0)
    lbl:SetTextColor(C.primary[1], C.primary[2], C.primary[3], 1)
    lbl:SetText(text)
    local line = Tex(parent, "BACKGROUND")
    line:SetHeight(1)
    line:SetPoint("TOPLEFT",  lbl,    "BOTTOMLEFT",  0, -3)
    line:SetPoint("TOPRIGHT", parent, "TOPRIGHT",    0, yOfs or 0)
    line:SetGradient("HORIZONTAL",
        CreateColor(C.primary[1], C.primary[2], C.primary[3], 0.7),
        CreateColor(0, 0, 0, 0))
    return lbl
end

local function HRule(parent, anchor, yOfs)
    local t = Tex(parent, "BACKGROUND"); t:SetHeight(1)
    t:SetPoint("TOPLEFT",  anchor or parent, "TOPLEFT",  0, yOfs or 0)
    t:SetPoint("TOPRIGHT", parent,           "TOPRIGHT", 0, yOfs or 0)
    t:SetColorTexture(unpack(C.separator))
    return t
end

-- ── modern slider ──────────────────────────────────────────────────────────
local function ModernSlider(parent, label, desc, minV, maxV, step, anchor, yOfs, getter, setter, fmt)
    local rowH = 58
    local con = CreateFrame("Frame", nil, parent); con:SetSize(CONTENT_W - 20, rowH)
    con:SetPoint("TOPLEFT", anchor or parent, "TOPLEFT", 0, yOfs or 0)

    local lbl = FS(con); lbl:SetPoint("TOPLEFT", 0, 0)
    lbl:SetTextColor(unpack(C.text)); lbl:SetText(label)
    local valLbl = FS(con, "GameFontNormalSmall")
    valLbl:SetPoint("TOPRIGHT", 0, -1)
    valLbl:SetTextColor(C.primary[1], C.primary[2], C.primary[3], 1)

    if desc then
        local d = FS(con, "GameFontHighlightSmall")
        d:SetPoint("TOPLEFT", lbl, "BOTTOMLEFT", 0, -1)
        d:SetTextColor(unpack(C.textDim)); d:SetText(desc)
    end

    local track = Tex(con, "BACKGROUND"); track:SetHeight(3)
    track:SetPoint("BOTTOMLEFT",  con, "BOTTOMLEFT",   6, 14)
    track:SetPoint("BOTTOMRIGHT", con, "BOTTOMRIGHT", -6, 14)
    track:SetColorTexture(0.14, 0.14, 0.14, 1)
    local fill = Tex(con, "ARTWORK"); fill:SetHeight(3)
    fill:SetPoint("LEFT", track, "LEFT", 0, 0)
    fill:SetColorTexture(C.primary[1], C.primary[2], C.primary[3], 0.85)

    local sl = CreateFrame("Slider", nil, con)
    sl:SetHeight(18)
    sl:SetPoint("LEFT",  track, "LEFT",  0, 0)
    sl:SetPoint("RIGHT", track, "RIGHT", 0, 0)
    sl:SetMinMaxValues(minV, maxV); sl:SetValueStep(step)
    sl:SetObeyStepOnDrag(true); sl:SetValue(getter())
    sl:SetOrientation("HORIZONTAL")
    local thumb = Tex(sl, "OVERLAY"); thumb:SetSize(10, 14)
    thumb:SetTexture("Interface\\Buttons\\WHITE8X8")
    thumb:SetVertexColor(C.primary[1], C.primary[2], C.primary[3], 1)
    sl:SetThumbTexture(thumb)

    local function UpdateFill()
        local pct = math.max(0, (sl:GetValue()-minV)/math.max(maxV-minV, 0.001))
        local w = track:GetWidth(); if w and w > 0 then fill:SetWidth(math.max(1, w*pct)) end
    end
    local function UpdateVal()
        local v = sl:GetValue()
        valLbl:SetText(fmt and fmt(v) or string.format("%.2f", v))
        UpdateFill(); setter(v)
    end
    sl:SetScript("OnValueChanged", UpdateVal)
    sl:SetScript("OnSizeChanged",  UpdateFill)
    sl:SetScript("OnEnter", function() thumb:SetVertexColor(C.primaryHi[1], C.primaryHi[2], C.primaryHi[3], 1) end)
    sl:SetScript("OnLeave", function() thumb:SetVertexColor(C.primary[1],   C.primary[2],   C.primary[3],   1) end)
    UpdateVal()
    con.slider  = sl
    con.Refresh = function() sl:SetValue(getter()); UpdateVal() end
    return con
end

-- ── modern toggle (rTooltip-Stil: Schiene + Kugel) ─────────────────────────
local TOGGLE_W = 36
local TOGGLE_H = 18
local KNOB_SZ  = 12

local function ModernCheckbox(parent, label, desc, anchor, yOfs, getter, setter)
    local rowH = desc and 52 or 34
    local con = CreateFrame("Frame", nil, parent)
    con:SetSize(CONTENT_W - 20, rowH)
    con:SetPoint("TOPLEFT", anchor or parent, "TOPLEFT", 0, yOfs or 0)

    local track = CreateFrame("Button", nil, con, "BackdropTemplate")
    track:SetSize(TOGGLE_W, TOGGLE_H)
    track:SetPoint("LEFT", con, "LEFT", 0, desc and 9 or 0)
    track:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        tile = false, edgeSize = 1, insets = { left = 0, right = 0, top = 0, bottom = 0 } })

    local knob = CreateFrame("Frame", nil, track, "BackdropTemplate")
    knob:SetSize(KNOB_SZ, KNOB_SZ)
    knob:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        tile = false, edgeSize = 1, insets = { left = 0, right = 0, top = 0, bottom = 0 } })

    local lbl = FS(con)
    lbl:SetPoint("LEFT", track, "RIGHT", 10, desc and 9 or 0)
    lbl:SetPoint("RIGHT", con, "RIGHT", -4, 0)
    lbl:SetJustifyH("LEFT")
    lbl:SetTextColor(unpack(C.text))
    lbl:SetText(label)

    if desc then
        local d = FS(con, "GameFontHighlightSmall")
        d:SetPoint("TOPLEFT", lbl, "BOTTOMLEFT", 0, -3)
        d:SetPoint("RIGHT", con, "RIGHT", -4, 0)
        d:SetJustifyH("LEFT")
        d:SetTextColor(unpack(C.textDim))
        d:SetText(desc)
    end

    local function SetVisual(on)
        if on then
            track:SetBackdropColor(C.primary[1] * 0.28, C.primary[2] * 0.20, C.primary[3] * 0.02, 1)
            track:SetBackdropBorderColor(C.primary[1] * 0.75, C.primary[2] * 0.60, C.primary[3] * 0.12, 1)
            knob:ClearAllPoints()
            knob:SetPoint("RIGHT", track, "RIGHT", -3, 0)
            knob:SetBackdropColor(C.primary[1], C.primary[2], C.primary[3], 1)
            knob:SetBackdropBorderColor(C.primaryHi[1], C.primaryHi[2], C.primaryHi[3], 1)
            lbl:SetTextColor(unpack(C.textBright))
        else
            track:SetBackdropColor(0.08, 0.09, 0.13, 1)
            track:SetBackdropBorderColor(unpack(C.border))
            knob:ClearAllPoints()
            knob:SetPoint("LEFT", track, "LEFT", 3, 0)
            knob:SetBackdropColor(0.30, 0.28, 0.25, 1)
            knob:SetBackdropBorderColor(0.22, 0.20, 0.18, 1)
            lbl:SetTextColor(unpack(C.text))
        end
    end

    local function Refresh()
        local v = getter()
        SetVisual(v == true or v == 1)
    end
    Refresh()

    track:SetScript("OnEnter", function()
        local on = (getter() == true or getter() == 1)
        if on then
            track:SetBackdropBorderColor(C.primaryHi[1], C.primaryHi[2], C.primaryHi[3], 1)
        else
            track:SetBackdropBorderColor(C.primary[1] * 0.55, C.primary[2] * 0.42, C.primary[3] * 0.15, 1)
        end
    end)
    track:SetScript("OnLeave", function()
        local on = (getter() == true or getter() == 1)
        if on then
            track:SetBackdropBorderColor(C.primary[1] * 0.75, C.primary[2] * 0.60, C.primary[3] * 0.12, 1)
        else
            track:SetBackdropBorderColor(unpack(C.border))
        end
    end)
    track:SetScript("OnClick", function()
        local newVal = not (getter() == true or getter() == 1)
        SetVisual(newVal)
        setter(newVal)
    end)

    con.Refresh = Refresh
    return con
end

-- ── color picker row (wie nCastbar: ColorPickerFrame:SetupColorPickerAndShow) ─
local function ModernColorPickerRow(parent, label, anchor, yOfs, getRgb, setRgb)
    local con = CreateFrame("Frame", nil, parent)
    con:SetSize(CONTENT_W - 20, 28)
    con:SetPoint("TOPLEFT", anchor or parent, "TOPLEFT", 0, yOfs or 0)

    local lbl = FS(con)
    lbl:SetPoint("LEFT", 0, 0)
    lbl:SetTextColor(unpack(C.text))
    lbl:SetText(label)

    local box = CreateFrame("Button", nil, con, "BackdropTemplate")
    box:SetSize(46, 20)
    box:SetPoint("RIGHT", 0, 0)
    box:SetBackdrop({
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        tile = false,
        edgeSize = 1,
        insets = { left = 0, right = 0, top = 0, bottom = 0 },
    })
    box:SetBackdropBorderColor(unpack(C.border))
    local bbg = box:CreateTexture(nil, "OVERLAY")
    bbg:SetAllPoints()

    local function UpdatePreview()
        local r, g, b = getRgb()
        if r and g and b then
            bbg:SetColorTexture(r, g, b, 1)
        else
            bbg:SetColorTexture(0.4, 0.4, 0.4, 1)
        end
    end
    UpdatePreview()

    box:SetScript("OnEnter", function()
        box:SetBackdropBorderColor(C.primary[1], C.primary[2], C.primary[3], 1)
    end)
    box:SetScript("OnLeave", function()
        box:SetBackdropBorderColor(unpack(C.border))
    end)
    box:SetScript("OnClick", function()
        local r, g, b = getRgb()
        ColorPickerFrame:SetupColorPickerAndShow({
            r = r,
            g = g,
            b = b,
            hasOpacity = false,
            swatchFunc = function()
                local nr, ng, nb = ColorPickerFrame:GetColorRGB()
                setRgb(nr, ng, nb)
                UpdatePreview()
            end,
            cancelFunc = function(p)
                setRgb(p.r, p.g, p.b)
                UpdatePreview()
            end,
        })
    end)
    con.Refresh = UpdatePreview
    return con
end

-- ── custom dropdown ────────────────────────────────────────────────────────
local activeDropdownPopup = nil
local dropdownBG

local function CloseActiveDropdown()
    if activeDropdownPopup then activeDropdownPopup:Hide(); activeDropdownPopup = nil end
    if dropdownBG then dropdownBG:Hide() end
end

local function ShowDropdownBG()
    if not dropdownBG then
        dropdownBG = CreateFrame("Button", "rMM_DropdownBG", UIParent)
        dropdownBG:SetAllPoints(UIParent)
        dropdownBG:SetFrameStrata("FULLSCREEN_DIALOG")
        dropdownBG:RegisterForClicks("AnyUp", "AnyDown")
        dropdownBG:SetScript("OnClick", CloseActiveDropdown)
        dropdownBG:EnableMouseWheel(true)
        dropdownBG:SetScript("OnMouseWheel", CloseActiveDropdown)
    end
    dropdownBG:Show()
end

--- Gespeicherte Texture-Pfade koennen von WoW/Hand leicht abweichen (/ vs \\, .tga, Leerzeichen).
local function NormalizeTexturePath(p)
    if type(p) ~= "string" then return "" end
    p = p:match("^%s*(.-)%s*$") or p
    p = p:lower():gsub("/", "\\")
    p = p:gsub("%.tga$", "")
    return p
end

local function DropdownValuesMatch(mode, optValue, current)
    if optValue == current then return true end
    if mode == "path" then
        return NormalizeTexturePath(optValue) == NormalizeTexturePath(current)
    end
    if type(optValue) == "string" and type(current) == "string" then
        return optValue:lower() == current:lower()
    end
    return false
end

--- width: Zahl oder nil; mode: nil|"path" (Overlay-Pfade) | "ci" (case-insensitive strings)
--- maxVisibleRows: nil = max. 8 Zeilen; bei mehr Eintraegen Scrollen (fuer Schriftarten-Listen)
local function ModernDropdown(parent, label, options, getter, setter, anchor, yOfs, width, mode, maxVisibleRows)
    local W = width or 280
    local BTN_H = 26
    local ITEM_H = 24
    local maxVis = maxVisibleRows or 8
    local shownRows = math.max(1, math.min(#options, maxVis))
    local needScroll = (#options > maxVis)
    local container = CreateFrame("Frame", nil, parent)
    container:SetSize(W, 52)
    container:SetPoint("TOPLEFT", anchor or parent, "TOPLEFT", 0, yOfs or 0)

    local lbl = FS(container, "GameFontHighlightSmall")
    lbl:SetPoint("TOPLEFT", 0, 0)
    lbl:SetTextColor(unpack(C.text)); lbl:SetText(label)

    local btn = CreateFrame("Button", nil, container, "BackdropTemplate")
    btn:SetSize(W, BTN_H)
    btn:SetPoint("TOPLEFT", 0, -20)
    btn:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8", edgeFile="Interface\\Buttons\\WHITE8X8",
        tile=false, edgeSize=1, insets={left=0,right=0,top=0,bottom=0}})
    btn:SetBackdropColor(0.08, 0.08, 0.08, 1)
    btn:SetBackdropBorderColor(unpack(C.border))

    local btnLbl = FS(btn, "GameFontHighlightSmall")
    btnLbl:SetPoint("LEFT", btn, "LEFT", 8, 0)
    btnLbl:SetTextColor(unpack(C.text))
    btnLbl:SetWidth(W - 20)
    btnLbl:SetJustifyH("LEFT")

    local rowRefreshers = {}

    local function UpdateDisplay()
        local val = getter()
        for _, opt in ipairs(options) do
            if DropdownValuesMatch(mode, opt.value, val) then btnLbl:SetText(opt.text); return end
        end
        btnLbl:SetText("\226\128\148")
    end
    UpdateDisplay()

    local popup = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    popup:SetFrameStrata("TOOLTIP"); popup:SetWidth(W)
    popup:SetHeight(shownRows * ITEM_H + 4)
    popup:Hide()
    popup:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8", edgeFile="Interface\\Buttons\\WHITE8X8",
        tile=false, edgeSize=1, insets={left=0,right=0,top=0,bottom=0}})
    popup:SetBackdropColor(0.07, 0.07, 0.07, 0.97)
    popup:SetBackdropBorderColor(C.primary[1]*0.6, C.primary[2]*0.5, C.primary[3]*0.9, 1)

    local rowParent = popup
    local scrollChild
    if needScroll then
        local sf = CreateFrame("ScrollFrame", nil, popup)
        sf:SetPoint("TOPLEFT", popup, "TOPLEFT", 1, -2)
        sf:SetPoint("BOTTOMRIGHT", popup, "BOTTOMRIGHT", -1, 2)
        scrollChild = CreateFrame("Frame", nil, sf)
        scrollChild:SetWidth(W - 4)
        scrollChild:SetHeight(math.max(#options, 1) * ITEM_H + 2)
        sf:SetScrollChild(scrollChild)
        sf:EnableMouseWheel(true)
        sf:SetScript("OnMouseWheel", function(self, delta)
            local cur = self:GetVerticalScroll()
            local step = ITEM_H * 3
            local maxScroll = math.max(0, scrollChild:GetHeight() - self:GetHeight())
            self:SetVerticalScroll(math.max(0, math.min(maxScroll, cur - delta * step)))
        end)
        rowParent = scrollChild
    end

    for i, opt in ipairs(options) do
        local row = CreateFrame("Button", nil, rowParent, "BackdropTemplate")
        local rin = needScroll and 0 or 1
        row:SetPoint("TOPLEFT",  rowParent, "TOPLEFT",  rin, -(i-1)*ITEM_H - 1)
        row:SetPoint("TOPRIGHT", rowParent, "TOPRIGHT", -rin, -(i-1)*ITEM_H - 1)
        row:SetHeight(ITEM_H)
        row:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8", tile=false, edgeSize=0, insets={left=0,right=0,top=0,bottom=0}})
        row:SetBackdropColor(0,0,0,0)

        local pip = Tex(row, "ARTWORK"); pip:SetWidth(2); pip:SetHeight(ITEM_H-8)
        pip:SetPoint("LEFT", row, "LEFT", 4, 0)
        pip:SetColorTexture(C.primary[1], C.primary[2], C.primary[3], 1)

        local rowLbl = FS(row, "GameFontHighlightSmall")
        rowLbl:SetPoint("LEFT", row, "LEFT", 12, 0)
        rowLbl:SetTextColor(unpack(C.text)); rowLbl:SetText(opt.text)

        local function RefreshRow()
            if DropdownValuesMatch(mode, opt.value, getter()) then
                rowLbl:SetTextColor(C.primaryHi[1], C.primaryHi[2], C.primaryHi[3], 1); pip:SetAlpha(1)
            else
                rowLbl:SetTextColor(unpack(C.text)); pip:SetAlpha(0)
            end
        end
        RefreshRow()
        rowRefreshers[#rowRefreshers + 1] = RefreshRow

        row:SetScript("OnEnter", function(s) s:SetBackdropColor(0.16, 0.16, 0.16, 1) end)
        row:SetScript("OnLeave", function(s) s:SetBackdropColor(0,0,0,0) end)
        row:SetScript("OnClick", function()
            setter(opt.value); UpdateDisplay(); CloseActiveDropdown()
        end)
    end

    btn:SetScript("OnEnter", function(s) s:SetBackdropBorderColor(C.primary[1], C.primary[2], C.primary[3], 1) end)
    btn:SetScript("OnLeave", function(s) s:SetBackdropBorderColor(unpack(C.border)) end)
    btn:SetScript("OnClick", function()
        if popup:IsShown() then CloseActiveDropdown(); return end
        CloseActiveDropdown()
        UpdateDisplay()
        for _, rf in ipairs(rowRefreshers) do rf() end
        popup:ClearAllPoints()
        local bLeft   = btn:GetLeft()
        local bBottom = btn:GetBottom()
        local bTop    = btn:GetTop()
        local popupVisH = shownRows * ITEM_H + 24
        if bLeft and bBottom and bBottom > popupVisH then
            popup:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", bLeft, bBottom - 2)
        elseif bLeft then
            popup:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", bLeft, (bTop or bBottom or 0) + 2)
        end
        ShowDropdownBG()
        popup:Show()
        activeDropdownPopup = popup
    end)

    container.Refresh = function()
        UpdateDisplay()
        for _, rf in ipairs(rowRefreshers) do rf() end
    end
    return container
end

-- ── styled button ──────────────────────────────────────────────────────────
local function StyledButton(parent, text, w, h, onClick)
    local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
    btn:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8", edgeFile="Interface\\Buttons\\WHITE8X8",
        tile=false, edgeSize=1, insets={left=0,right=0,top=0,bottom=0}})
    btn:SetBackdropColor(C.primary[1]*0.12, C.primary[2]*0.08, C.primary[3]*0.12, 1)
    btn:SetBackdropBorderColor(C.primary[1]*0.55, C.primary[2]*0.45, C.primary[3]*0.80, 1)
    local lbl = FS(btn); lbl:SetPoint("CENTER")
    lbl:SetTextColor(C.primary[1], C.primary[2], C.primary[3], 1); lbl:SetText(text)
    btn:SetSize(w or 110, h or 26)
    btn:SetScript("OnEnter", function(s)
        s:SetBackdropColor(C.primary[1]*0.28, C.primary[2]*0.22, C.primary[3]*0.32, 1)
        s:SetBackdropBorderColor(C.primaryHi[1], C.primaryHi[2], C.primaryHi[3], 1)
        lbl:SetTextColor(1, 1, 1, 1)
    end)
    btn:SetScript("OnLeave", function(s)
        s:SetBackdropColor(C.primary[1]*0.12, C.primary[2]*0.08, C.primary[3]*0.12, 1)
        s:SetBackdropBorderColor(C.primary[1]*0.55, C.primary[2]*0.45, C.primary[3]*0.80, 1)
        lbl:SetTextColor(C.primary[1], C.primary[2], C.primary[3], 1)
    end)
    btn:SetScript("OnClick", onClick)
    return btn
end

-- ── fade helpers ───────────────────────────────────────────────────────────
local function ConfigFadeIn(f)  f:SetAlpha(0); f:Show(); UIFrameFadeIn(f, 0.16, 0, 1) end
local function ConfigFadeOut(f)
    UIFrameFadeOut(f, 0.20, f:GetAlpha(), 0)
    local elapsed, ticker = 0
    ticker = C_Timer.NewTicker(0.05, function()
        elapsed = elapsed + 0.05
        if elapsed >= 0.20 then f:Hide(); f:SetAlpha(1); ticker:Cancel() end
    end)
end

-- ── overlay definitions ────────────────────────────────────────────────────
-- Silberner Rand INNEN am Kreis: meist kein einzelnes Addon-File.
--   - hauefig: Kanten des kreisfoermigen Minimap-Renderings (Maske / AA), nicht MinimapBorder.
--   - zusaetzlich kann media/map_gloss.tga (ADD, SubLayer -3) und/oder Blizzard z.B.
--     MinimapBackdrop (wird in core ausgeblendet) optisch mitspielen.
-- Zum Debuggen im Spiel: /fstack aktivieren und mit der Maus ueber den Ring fahren.
-- Border hoeher schieben: ofsY positiv = Textur-Mitte nach oben (mehr freier Rand unten).
-- 512x256 (2:1) vs. 1536x1024 (1.5:1): adjust = Hoehe, wMul = Breite/Hoehe.
ns.OVERLAY_DEFS = {
    ["Interface\\AddOns\\rMinimap\\media\\map_overlay"] = {
        label = "Classic",
        adjust = 1.3,
        wMul = 2,
        ofsX = 0, ofsY = 0,
    },
    ["Interface\\AddOns\\rMinimap\\media\\map_overlay_onlyring"] = {
        label = "Ring Only",
        adjust = 1.3,
        wMul = 2,
        ofsX = 0, ofsY = 0,
    },
    ["Interface\\AddOns\\rMinimap\\media\\map_overlay_new"] = {
        label = "Modern (with wings)",
        adjust = 1.50,
        wMul = 1.5,
        ofsX = 0, ofsY = -22,
    },
    ["Interface\\AddOns\\rMinimap\\media\\map_overlay_new(withoutwings)"] = {
        label = "Modern (without wings)",
        adjust = 1.50,
        wMul = 1.5,
        ofsX = 0, ofsY = -22,
    },
}

local OVERLAY_OPTIONS = {}
for path, def in pairs(ns.OVERLAY_DEFS) do
    OVERLAY_OPTIONS[#OVERLAY_OPTIONS+1] = { text = def.label, value = path }
end
table.sort(OVERLAY_OPTIONS, function(a,b) return a.text < b.text end)

--- Zonentext: WoW-Basis + LibSharedMedia (wie CombatStatus: Fetch per Key)
local RMM_FONT_PATH = "path$"
local RMM_FONT_LSM  = "lsm$"

local function NormalizeFontPathForDedup(p)
    if type(p) ~= "string" then return "" end
    return p:lower():gsub("/", "\\")
end

local function BuildZoneFontOptions()
    local opts = {}
    local seenPaths = {}
    local function markSeen(path)
        local n = NormalizeFontPathForDedup(path)
        if n ~= "" then seenPaths[n] = true end
    end
    local function addPath(label, path)
        if type(path) ~= "string" or path == "" then return end
        local n = NormalizeFontPathForDedup(path)
        if n == "" or seenPaths[n] then return end
        markSeen(path)
        opts[#opts + 1] = { text = label, value = RMM_FONT_PATH .. path }
    end

    addPath("Standard", STANDARD_TEXT_FONT)
    addPath("Friz Quadrata", "Fonts\\FRIZQT__.TTF")
    addPath("Arial Narrow", "Fonts\\ARIALN.TTF")
    addPath("Morpheus", "Fonts\\MORPHEUS.TTF")
    addPath("Skurri", "Fonts\\SKURRI_CYR.ttf")

    local LSM = LibStub and LibStub("LibSharedMedia-3.0", true)
    if LSM then
        local names = LSM:List("font")
        local sorted = {}
        for i = 1, #names do sorted[i] = names[i] end
        table.sort(sorted, function(a, b) return tostring(a):lower() < tostring(b):lower() end)
        for _, name in ipairs(sorted) do
            local path = LSM:Fetch("font", name)
            if type(path) == "string" and path ~= "" then
                local n = NormalizeFontPathForDedup(path)
                if n ~= "" and not seenPaths[n] then
                    markSeen(path)
                    opts[#opts + 1] = { text = tostring(name), value = RMM_FONT_LSM .. tostring(name) }
                end
            end
        end
    end

    table.sort(opts, function(a, b) return a.text:lower() < b.text:lower() end)
    return opts
end

local function EnsureCurrentZoneFontInOptions(opts, cfg_)
    local curVal
    if rMinimapDB.zoneFontMediaKey and rMinimapDB.zoneFontMediaKey ~= "" then
        curVal = RMM_FONT_LSM .. rMinimapDB.zoneFontMediaKey
    else
        local p = rMinimapDB.zoneFontFamily or cfg_.info.zoneFont.family
        if type(p) == "string" and p ~= "" then
            curVal = RMM_FONT_PATH .. p
        end
    end
    if not curVal then return end
    for _, o in ipairs(opts) do
        if o.value == curVal then return end
    end
    if curVal:sub(1, #RMM_FONT_LSM) == RMM_FONT_LSM then
        local key = curVal:sub(#RMM_FONT_LSM + 1)
        tinsert(opts, 1, { text = key .. " (LSM)", value = curVal })
    else
        tinsert(opts, 1, { text = T("ZONE_TEXT_FONT_SAVED", "Saved font (path)"), value = curVal })
    end
end

local function ApplyOverlayTexture(path)
    rMinimapDB.overlayTexture = path
    local t = ns.overlayTex
    if not t then return end
    t:SetTexture(path)
    local def = ns.OVERLAY_DEFS[path] or {}
    local adjust = def.adjust or 1.3
    local wMul   = def.wMul   or 2
    local ofsX   = def.ofsX   or 0
    local ofsY   = def.ofsY   or 0
    t:ClearAllPoints()
    t:SetPoint("CENTER", ofsX, ofsY)
    t:SetWidth(Minimap:GetHeight() * wMul * adjust)
    t:SetHeight(Minimap:GetHeight() * adjust)
    if ns.ApplyPortalLayoutFromDB then ns.ApplyPortalLayoutFromDB() end
end

-- ── build GUI ──────────────────────────────────────────────────────────────
local configFrame

local function BuildConfigFrame()
    if configFrame then return configFrame end

    local frame = CreateFrame("Frame", "rMinimapConfigFrame", UIParent, "BackdropTemplate")
    frame:SetSize(FRAME_W, FRAME_H)
    frame:SetPoint("CENTER")
    frame:SetMovable(true); frame:EnableMouse(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetScript("OnDragStart", frame.StartMoving)
    frame:SetScript("OnDragStop",  frame.StopMovingOrSizing)
    frame:SetFrameStrata("DIALOG"); frame:SetClampedToScreen(true); frame:Hide()
    frame.__rmmDropdownRefreshes = {}
    frame:HookScript("OnShow", function(self) self:EnableKeyboard(true) end)
    frame:HookScript("OnShow", function(self)
        local list = self.__rmmDropdownRefreshes
        if not list then return end
        for _, fn in ipairs(list) do pcall(fn) end
    end)
    frame:HookScript("OnHide", function(self) self:EnableKeyboard(false); CloseActiveDropdown() end)
    frame:SetScript("OnKeyDown", function(self, key)
        if key == "ESCAPE" then
            if activeDropdownPopup then
                CloseActiveDropdown()
            else
                ConfigFadeOut(self)
            end
            self:SetPropagateKeyboardInput(false)
        else
            self:SetPropagateKeyboardInput(true)
        end
    end)
    frame:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8", edgeFile="Interface\\Buttons\\WHITE8X8",
        tile=false, edgeSize=2, insets={left=0,right=0,top=0,bottom=0}})
    frame:SetBackdropColor(unpack(C.bg))
    frame:SetBackdropBorderColor(C.primary[1]*0.5, C.primary[2]*0.4, C.primary[3]*0.7, 1)

    -- ── title bar ──────────────────────────────────────────────────────────
    local titleBg = FlatFrame(frame,
        C.primary[1]*0.10, C.primary[2]*0.08, C.primary[3]*0.14, 1,
        C.primary[1]*0.35, C.primary[2]*0.28, C.primary[3]*0.55, 1)
    titleBg:SetPoint("TOPLEFT",  frame, "TOPLEFT",   2, -2)
    titleBg:SetPoint("TOPRIGHT", frame, "TOPRIGHT",  -2, -2)
    titleBg:SetHeight(TITLE_H)

    local ta = Tex(titleBg); ta:SetWidth(3)
    ta:SetPoint("TOPLEFT",    titleBg, "TOPLEFT",    0, 0)
    ta:SetPoint("BOTTOMLEFT", titleBg, "BOTTOMLEFT", 0, 0)
    ta:SetColorTexture(C.primary[1], C.primary[2], C.primary[3], 1)

    local logoTex = Tex(titleBg)
    logoTex:SetTexture(7439196) -- Spell icon fileID
    logoTex:SetSize(28, 28)
    logoTex:SetPoint("LEFT", titleBg, "LEFT", 14, 0)

    local titleLbl = FS(titleBg, "GameFontNormalHuge")
    titleLbl:SetPoint("LEFT", logoTex, "RIGHT", 10, 0)
    titleLbl:SetTextColor(1, 1, 1, 1)
    titleLbl:SetText(T("TITLE", "rMinimap"))

    local tgl = Tex(titleBg); tgl:SetHeight(2)
    tgl:SetPoint("BOTTOMLEFT",  titleBg, "BOTTOMLEFT",  0, 0)
    tgl:SetPoint("BOTTOMRIGHT", titleBg, "BOTTOMRIGHT", 0, 0)
    tgl:SetGradient("HORIZONTAL",
        CreateColor(C.primary[1], C.primary[2], C.primary[3], 1),
        CreateColor(0, 0, C.primary[3]*0.2, 0))

    -- close
    local closeBtn = CreateFrame("Button", nil, titleBg)
    closeBtn:SetSize(28, 28)
    closeBtn:SetPoint("RIGHT", titleBg, "RIGHT", -14, 0)
    local closeBg = closeBtn:CreateTexture(nil, "BACKGROUND")
    closeBg:SetAllPoints(); closeBg:SetColorTexture(0.45, 0.08, 0.08, 0.8)
    local closeLbl = closeBtn:CreateFontString(nil, "OVERLAY")
    closeLbl:SetFont("Fonts\\FRIZQT__.TTF", 16, "OUTLINE")
    closeLbl:SetPoint("CENTER", 0, 1); closeLbl:SetText("x"); closeLbl:SetTextColor(1,1,1,1)
    closeBtn:SetScript("OnEnter", function() closeBg:SetColorTexture(0.90, 0.22, 0.22, 1) end)
    closeBtn:SetScript("OnLeave", function() closeBg:SetColorTexture(0.45, 0.08, 0.08, 0.8) end)
    closeBtn:SetScript("OnClick", function() ConfigFadeOut(frame) end)

    -- ── sidebar + content area (nBuff-style) ──────────────────────────────
    local sidebar = FlatFrame(frame, 0.055, 0.055, 0.055, 1.0, 0.12, 0.12, 0.12, 1.0)
    sidebar:SetPoint("TOPLEFT",    titleBg, "BOTTOMLEFT", 2, 0)
    sidebar:SetPoint("BOTTOMLEFT", frame,   "BOTTOMLEFT", 2, BOT_H+2)
    sidebar:SetWidth(SIDEBAR_W)

    local sbl = Tex(sidebar); sbl:SetWidth(1)
    sbl:SetPoint("TOPRIGHT",    sidebar, "TOPRIGHT",    0, 0)
    sbl:SetPoint("BOTTOMRIGHT", sidebar, "BOTTOMRIGHT", 0, 0)
    sbl:SetColorTexture(0.18, 0.18, 0.18, 1)

    local contentBg = FlatFrame(frame, 0.07, 0.07, 0.07, 0.97, unpack(C.border))
    contentBg:SetPoint("TOPLEFT", sidebar, "TOPRIGHT", 1, 0)
    contentBg:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -2, BOT_H+2)

    -- ── bottom bar ─────────────────────────────────────────────────────────
    local bot = FlatFrame(frame, 0.07, 0.07, 0.07, 1, 0.18, 0.18, 0.18, 1)
    bot:SetPoint("BOTTOMLEFT",  frame, "BOTTOMLEFT",   2, 2)
    bot:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -2, 2)
    bot:SetHeight(BOT_H)
    local botLine = Tex(bot); botLine:SetHeight(1)
    botLine:SetPoint("TOPLEFT",  bot, "TOPLEFT",  0, 0)
    botLine:SetPoint("TOPRIGHT", bot, "TOPRIGHT", 0, 0)
    botLine:SetColorTexture(C.primary[1], C.primary[2], C.primary[3], 0.35)

    -- GUI Scale (wie nBuff unten links)
    do
        local SCALE_STEP = 0.05
        local SCALE_MIN  = 0.5
        local SCALE_MAX  = 2.0

        local scaleLbl = FS(bot, "GameFontHighlightSmall")
        scaleLbl:SetPoint("LEFT", bot, "LEFT", 10, 0)
        scaleLbl:SetTextColor(unpack(C.textDim))
        scaleLbl:SetText(T("GUI_SCALE", "GUI Scale"))

        local function GetScale()
            return (rMinimapDB and rMinimapDB.guiScale) or 1.0
        end
        local function ApplyScale(v)
            v = math.max(SCALE_MIN, math.min(SCALE_MAX, v))
            v = math.floor(v / SCALE_STEP + 0.5) * SCALE_STEP
            v = math.floor(v * 100 + 0.5) / 100
            rMinimapDB.guiScale = v
            frame:SetScale(v)
        end

        local valBox = CreateFrame("EditBox", nil, bot, "BackdropTemplate")
        valBox:SetSize(46, 22)
        valBox:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8x8", edgeFile="Interface\\Buttons\\WHITE8x8", edgeSize=1, insets={left=0,right=0,top=0,bottom=0}})
        valBox:SetBackdropColor(0.04, 0.05, 0.09, 0.9)
        valBox:SetBackdropBorderColor(C.primary[1]*0.4, C.primary[2]*0.35, C.primary[3]*0.6, 1)
        valBox:SetFont("Fonts\\FRIZQT__.TTF", 11, "")
        valBox:SetTextColor(0.8, 0.9, 1.0, 1)
        valBox:SetJustifyH("CENTER")
        valBox:SetMaxLetters(5)
        valBox:SetAutoFocus(false)
        valBox:EnableMouseWheel(true)

        local function SyncScaleBox()
            local v = GetScale()
            valBox:SetText(string.format("%.2f", v))
            frame:SetScale(v)
        end

        local function MakeArrowBtn(label, dir)
            local b = CreateFrame("Button", nil, bot, "BackdropTemplate")
            b:SetSize(20, 22)
            b:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8x8", edgeFile="Interface\\Buttons\\WHITE8x8", edgeSize=1, insets={left=1,right=1,top=1,bottom=1}})
            b:SetBackdropColor(0.06, 0.08, 0.14, 1)
            b:SetBackdropBorderColor(C.primary[1]*0.45, C.primary[2]*0.38, C.primary[3]*0.65, 1)
            local t = b:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            t:SetAllPoints(); t:SetText(label)
            t:SetTextColor(C.primaryHi[1], C.primaryHi[2], C.primaryHi[3], 1)
            b:SetScript("OnEnter", function(s)
                s:SetBackdropColor(0.10, 0.14, 0.24, 1)
                s:SetBackdropBorderColor(C.primary[1], C.primary[2], C.primary[3], 1)
            end)
            b:SetScript("OnLeave", function(s)
                s:SetBackdropColor(0.06, 0.08, 0.14, 1)
                s:SetBackdropBorderColor(C.primary[1]*0.45, C.primary[2]*0.38, C.primary[3]*0.65, 1)
                s.held = false
            end)
            local ticker
            b:SetScript("OnClick", function()
                ApplyScale(GetScale() + dir * SCALE_STEP)
                SyncScaleBox()
            end)
            b:SetScript("OnMouseDown", function()
                b.held = true
                C_Timer.After(0.4, function()
                    if b.held then
                        ticker = C_Timer.NewTicker(0.08, function()
                            if b.held then
                                ApplyScale(GetScale() + dir * SCALE_STEP)
                                SyncScaleBox()
                            else
                                ticker:Cancel(); ticker = nil
                            end
                        end)
                    end
                end)
            end)
            b:SetScript("OnMouseUp", function()
                b.held = false
                if ticker then ticker:Cancel(); ticker = nil end
            end)
            return b
        end

        local btnMinus = MakeArrowBtn("<", -1)
        btnMinus:SetPoint("LEFT", scaleLbl, "RIGHT", 8, 0)
        valBox:SetPoint("LEFT", btnMinus, "RIGHT", 2, 0)
        local btnPlus = MakeArrowBtn(">", 1)
        btnPlus:SetPoint("LEFT", valBox, "RIGHT", 2, 0)

        valBox:SetScript("OnMouseWheel", function(_, d)
            ApplyScale(GetScale() + d * SCALE_STEP)
            SyncScaleBox()
        end)
        local function CommitTyped()
            local v = tonumber(valBox:GetText())
            if v then ApplyScale(v) end
            SyncScaleBox()
            valBox:ClearFocus()
        end
        valBox:SetScript("OnEnterPressed", CommitTyped)
        valBox:SetScript("OnTabPressed", CommitTyped)
        valBox:SetScript("OnEditFocusLost", function() C_Timer.After(0, CommitTyped) end)
        valBox:SetScript("OnEscapePressed", function() SyncScaleBox(); valBox:ClearFocus() end)

        frame:HookScript("OnShow", SyncScaleBox)
        frame:HookScript("OnHide", function() valBox:ClearFocus() end)
        frame.SyncScale = SyncScaleBox
        SyncScaleBox()
    end

    StyledButton(bot, T("CLOSE", "Close"), 90, 26, function() ConfigFadeOut(frame) end)
        :SetPoint("RIGHT", bot, "RIGHT", -12, 0)

    local cfg = ns.cfg
    local TAB_DEFS = {
        { label = T("TAB_GENERAL", "General"), icon = "Interface\\Icons\\INV_Misc_Note_06" },
        { label = T("TAB_VISUALS", "Visuals"), icon = "Interface\\Icons\\INV_Misc_ArmorKit_17" },
        { label = T("TAB_EFFECTS", "Effects"), icon = "Interface\\Icons\\Spell_Fire_Fire" },
        { label = T("TAB_ABOUT", "About"),   icon = "Interface\\Icons\\INV_Misc_Book_09" },
    }
    local tabs, tabPanels = {}, {}
    local activeTab = 1

    local contentHeader = FS(contentBg, "GameFontNormalLarge")
    contentHeader:SetPoint("TOPLEFT", contentBg, "TOPLEFT", PAD, -14)
    contentHeader:SetTextColor(C.primary[1], C.primary[2], C.primary[3], 0.7)
    local chLine = Tex(contentBg, "BACKGROUND"); chLine:SetHeight(1)
    chLine:SetPoint("TOPLEFT",  contentBg, "TOPLEFT",  PAD,  -34)
    chLine:SetPoint("TOPRIGHT", contentBg, "TOPRIGHT", -PAD, -34)
    chLine:SetColorTexture(C.primary[1], C.primary[2], C.primary[3], 0.20)

    local function MakeScroll(parent)
        local TRACK_W = 8
        local container = CreateFrame("Frame", nil, parent)
        container:SetAllPoints(parent)

        local sf = CreateFrame("ScrollFrame", nil, container)
        sf:SetPoint("TOPLEFT", container, "TOPLEFT", 8, -40)
        sf:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", -(TRACK_W + 4), 4)
        sf:EnableMouseWheel(true)

        local sc = CreateFrame("Frame", nil, sf)
        sc:SetWidth(CONTENT_W - 30)
        sf:SetScrollChild(sc)

        local track = CreateFrame("Frame", nil, container)
        track:SetSize(TRACK_W, 10)
        track:SetPoint("TOPRIGHT", container, "TOPRIGHT", -2, -40)
        track:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", -2, 4)
        local trackTex = track:CreateTexture(nil, "BACKGROUND")
        trackTex:SetAllPoints(track)
        trackTex:SetColorTexture(0.10, 0.10, 0.10, 1)

        local thumb = CreateFrame("Button", nil, track, "BackdropTemplate")
        thumb:SetWidth(TRACK_W)
        thumb:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8", edgeFile="Interface\\Buttons\\WHITE8X8", edgeSize=1, insets={left=0,right=0,top=0,bottom=0}})
        thumb:SetBackdropColor(C.primary[1]*0.55, C.primary[2]*0.22, C.primary[3]*0.80, 1)
        thumb:SetBackdropBorderColor(C.primary[1]*0.80, C.primary[2]*0.45, C.primary[3]*1.0, 1)
        thumb:SetPoint("TOPLEFT", track, "TOPLEFT", 0, 0)

        local function UpdateThumb()
            local trackH = track:GetHeight()
            if not trackH or trackH <= 0 then return end
            local viewH = sf:GetHeight()
            local contentH = sc:GetHeight()
            if contentH <= viewH then
                thumb:Hide()
                return
            end
            thumb:Show()
            local ratio = viewH / contentH
            local thumbH = math.max(20, trackH * ratio)
            thumb:SetHeight(thumbH)
            local maxScroll = contentH - viewH
            local cur = sf:GetVerticalScroll()
            local pct = maxScroll > 0 and (cur / maxScroll) or 0
            local maxY = trackH - thumbH
            thumb:SetPoint("TOPLEFT", track, "TOPLEFT", 0, -(pct * maxY))
        end

        sf:SetScript("OnMouseWheel", function(self, d)
            CloseActiveDropdown()
            local viewH = self:GetHeight()
            local contentH = sc:GetHeight()
            local maxScroll = math.max(0, contentH - viewH)
            local cur = self:GetVerticalScroll()
            self:SetVerticalScroll(math.max(0, math.min(maxScroll, cur - d * 30)))
            UpdateThumb()
        end)

        local dragStartY, dragStartScroll
        thumb:RegisterForClicks("LeftButtonUp")
        thumb:EnableMouse(true)
        thumb:SetScript("OnMouseDown", function(self, btn)
            if btn ~= "LeftButton" then return end
            local _, my = GetCursorPosition()
            local scale = UIParent:GetEffectiveScale()
            dragStartY = my / scale
            dragStartScroll = sf:GetVerticalScroll()
            self:SetScript("OnUpdate", function()
                local _, cy = GetCursorPosition()
                cy = cy / scale
                local delta = dragStartY - cy
                local trackH = track:GetHeight()
                local viewH = sf:GetHeight()
                local contentH = sc:GetHeight()
                local maxScroll = math.max(0, contentH - viewH)
                local thumbH = self:GetHeight()
                local maxY = trackH - thumbH
                if maxY <= 0 then return end
                sf:SetVerticalScroll(math.max(0, math.min(maxScroll, dragStartScroll + (delta / maxY) * maxScroll)))
                UpdateThumb()
            end)
        end)
        thumb:SetScript("OnMouseUp", function(self)
            self:SetScript("OnUpdate", nil)
        end)
        thumb:SetScript("OnEnter", function(self)
            self:SetBackdropColor(C.primary[1]*0.80, C.primary[2]*0.35, C.primary[3]*1.0, 1)
        end)
        thumb:SetScript("OnLeave", function(self)
            self:SetBackdropColor(C.primary[1]*0.55, C.primary[2]*0.22, C.primary[3]*0.80, 1)
        end)

        track:EnableMouse(true)
        track:SetScript("OnMouseDown", function(self, btn)
            if btn ~= "LeftButton" then return end
            local _, my = GetCursorPosition()
            my = my / UIParent:GetEffectiveScale()
            local trackH = self:GetHeight()
            local viewH = sf:GetHeight()
            local contentH = sc:GetHeight()
            local maxScroll = math.max(0, contentH - viewH)
            local _, screenCY = self:GetCenter()
            local trackTop = (screenCY or 0) + trackH / 2
            local relY = trackTop - my
            local pct = math.max(0, math.min(1, relY / trackH))
            sf:SetVerticalScroll(pct * maxScroll)
            UpdateThumb()
        end)

        sf:SetScript("OnScrollRangeChanged", function() UpdateThumb() end)
        sc:SetScript("OnSizeChanged", function() UpdateThumb() end)
        sf:SetScript("OnSizeChanged", function() UpdateThumb() end)
        C_Timer.After(0, UpdateThumb)
        return container, sc
    end

    local function SelectTab(idx)
        activeTab = idx
        contentHeader:SetText(TAB_DEFS[idx].label)
        for i, tab in ipairs(tabs) do
            local selected = (i == idx)
            tab.accent:SetShown(selected)
            if selected then
                tab.bgTex:SetColorTexture(0.14, 0.10, 0.04, 1)
                tab.lbl:SetTextColor(1, 0.96, 1.00, 1)
                tab.icon:SetVertexColor(1, 0.90, 1.00, 1)
            else
                tab.bgTex:SetColorTexture(0.055, 0.055, 0.055, 1)
                tab.lbl:SetTextColor(0.68, 0.70, 0.80, 1)
                tab.icon:SetVertexColor(0.42, 0.44, 0.55, 1)
            end
            tabPanels[i]:SetShown(selected)
        end
    end

    local TAB_H, TAB_GAP = 40, 2
    for i, td in ipairs(TAB_DEFS) do
        local tab = CreateFrame("Button", nil, sidebar)
        tab:SetHeight(TAB_H)
        tab:SetPoint("TOPLEFT", sidebar, "TOPLEFT", 0, -(i-1)*(TAB_H+TAB_GAP))
        tab:SetPoint("TOPRIGHT", sidebar, "TOPRIGHT", -1, -(i-1)*(TAB_H+TAB_GAP))
        tab.bgTex = Tex(tab, "BACKGROUND"); tab.bgTex:SetAllPoints()
        tab.bgTex:SetColorTexture(0.055, 0.055, 0.055, 1)
        tab.accent = Tex(tab, "OVERLAY"); tab.accent:SetWidth(3)
        tab.accent:SetPoint("TOPLEFT", tab, "TOPLEFT", 0, 0)
        tab.accent:SetPoint("BOTTOMLEFT", tab, "BOTTOMLEFT", 0, 0)
        tab.accent:SetColorTexture(C.primary[1], C.primary[2], C.primary[3], 1)
        tab.accent:Hide()
        tab.icon = Tex(tab, "OVERLAY"); tab.icon:SetSize(18, 18)
        tab.icon:SetPoint("LEFT", tab, "LEFT", 14, 0)
        tab.icon:SetTexture(td.icon)
        tab.icon:SetVertexColor(0.42, 0.44, 0.55, 1)
        tab.lbl = FS(tab, "GameFontNormalSmall")
        tab.lbl:SetPoint("LEFT", tab.icon, "RIGHT", 8, 0)
        tab.lbl:SetPoint("RIGHT", tab, "RIGHT", -6, 0)
        tab.lbl:SetJustifyH("LEFT")
        tab.lbl:SetTextColor(0.68, 0.70, 0.80, 1)
        tab.lbl:SetText(td.label)
        tab:SetScript("OnEnter", function(self)
            if activeTab ~= i then
                self.bgTex:SetColorTexture(0.10, 0.10, 0.10, 1)
                self.lbl:SetTextColor(C.primaryHi[1], C.primaryHi[2], C.primaryHi[3], 1)
                self.icon:SetVertexColor(C.primary[1], C.primary[2], C.primary[3], 1)
            end
        end)
        tab:SetScript("OnLeave", function(self)
            if activeTab ~= i then
                self.bgTex:SetColorTexture(0.055, 0.055, 0.055, 1)
                self.lbl:SetTextColor(0.68, 0.70, 0.80, 1)
                self.icon:SetVertexColor(0.42, 0.44, 0.55, 1)
            end
        end)
        tab:SetScript("OnClick", function() SelectTab(i) end)
        tabs[i] = tab
    end

    -- Tab 1: General
    local panelG, sG = MakeScroll(contentBg)
    do
        local y = -PAD
        local logoHolder = CreateFrame("Frame", nil, sG)
        logoHolder:SetHeight(170)
        logoHolder:SetPoint("TOPLEFT", sG, "TOPLEFT", 0, y)
        logoHolder:SetPoint("TOPRIGHT", sG, "TOPRIGHT", 0, y)
        local logoImg = Tex(logoHolder, "ARTWORK")
        logoImg:SetTexture("Interface\\AddOns\\rMinimap\\media\\logo_big")
        logoImg:SetSize(240, 160)
        logoImg:SetPoint("CENTER", logoHolder, "CENTER", 0, 0)
        y = y - 178

        SectionHeader(sG, T("FRAME_POSITION", "Frame positioning"), sG, 0, y); y = y - 32
        local dragLockToggle = ModernCheckbox(sG, T("FRAME_DRAG_LOCK", "Lock movement and scaling"), T("FRAME_DRAG_LOCK_DESC", ""), sG, y,
            function() return rMinimapDB.frameDragLocked == true end,
            function(on)
                rMinimapDB.frameDragLocked = on
            end)
        tinsert(frame.__rmmDropdownRefreshes, function() dragLockToggle.Refresh() end)
        y = y - 52
        sG:SetHeight(math.abs(y) + 40)
    end
    tabPanels[1] = panelG

    -- Tab 2: Visuals (Optik): Uhr, Minimap-Massstab, Infotext, Rand, Zahnrad
    local panelV, sV = MakeScroll(contentBg); panelV:Hide()
    do
        local y = -PAD
        SectionHeader(sV, T("CLOCK", "Clock"), sV, 0, y); y = y - 32
        local clockShowToggle = ModernCheckbox(sV, T("SHOW_CLOCK", "Show clock"), nil, sV, y,
            function() return rMinimapDB.clockShow ~= false end,
            function(on)
                rMinimapDB.clockShow = on
                if ns.ApplyMinimapClockFromDB then ns.ApplyMinimapClockFromDB() end
            end)
        tinsert(frame.__rmmDropdownRefreshes, function() clockShowToggle.Refresh() end)
        y = y - 34

        ModernSlider(sV, T("CLOCK_FONT_SIZE", "Clock Font Size"), T("CLOCK_FONT_SIZE_DESC", "Size of the minimap clock text"), 8, 30, 1, sV, y,
            function() return rMinimapDB.clockFontSize or cfg.clock.font.size end,
            function(v)
                v = math.floor(v)
                rMinimapDB.clockFontSize = v
                if ns.ApplyMinimapClockFromDB then ns.ApplyMinimapClockFromDB() end
            end,
            function(v) return math.floor(v) .. "pt" end)
        y = y - 68

        ModernSlider(sV, T("CLOCK_X_OFFSET", "Clock X Offset"), T("CLOCK_X_OFFSET_DESC", "Horizontal offset from default position (config)"), -80, 80, 1, sV, y,
            function() return rMinimapDB.clockPosX or 0 end,
            function(v)
                v = math.floor(v)
                rMinimapDB.clockPosX = v
                if ns.ApplyMinimapClockFromDB then ns.ApplyMinimapClockFromDB() end
            end,
            function(v) return math.floor(v) .. "px" end)
        y = y - 68

        ModernSlider(sV, T("CLOCK_Y_OFFSET", "Clock Y Offset"), T("CLOCK_Y_OFFSET_DESC", "Vertical offset from default position (config)"), -80, 80, 1, sV, y,
            function() return rMinimapDB.clockPosY or 0 end,
            function(v)
                v = math.floor(v)
                rMinimapDB.clockPosY = v
                if ns.ApplyMinimapClockFromDB then ns.ApplyMinimapClockFromDB() end
            end,
            function(v) return math.floor(v) .. "px" end)
        y = y - 68

        HRule(sV, sV, y); y = y - 22
        SectionHeader(sV, T("MINIMAP", "Minimap"), sV, 0, y); y = y - 32
        ModernSlider(sV, T("MINIMAP_SCALE", "Minimap Scale"), T("MINIMAP_SCALE_DESC", "Scale of the entire minimap cluster"), 0.5, 3.0, 0.01, sV, y,
            function() return MinimapCluster and MinimapCluster:GetScale() or (rMinimapDB.clusterScale or cfg.mapcluster.scale) end,
            function(v)
                rMinimapDB.clusterScale = v
                if MinimapCluster then MinimapCluster:SetScale(v) end
            end,
            function(v) return string.format("%.0f%%", v * 100) end)
        y = y - 68

        HRule(sV, sV, y); y = y - 22
        SectionHeader(sV, T("INFO_TEXT", "Info Text"), sV, 0, y); y = y - 32
        local PLACEMENT_OPTS = {
            { text = T("INFO_TEXT_BELOW", "Below minimap"), value = "below" },
            { text = T("INFO_TEXT_ABOVE", "Above minimap"), value = "above" },
        }
        local ddInfoPlace = ModernDropdown(sV, T("INFO_TEXT_PLACEMENT", "Info text position"), PLACEMENT_OPTS,
            function() return rMinimapDB.infoTextPlacement or "below" end,
            function(v)
                rMinimapDB.infoTextPlacement = v
                if v == "above" and rMinimapDB.infoTextYAbove == nil then
                    rMinimapDB.infoTextYAbove = cfg.info.posAboveY or 110
                end
                if ns.ApplyInfoTextLayoutFromDB then ns.ApplyInfoTextLayoutFromDB() end
            end,
            sV, y, CONTENT_W - 30)
        tinsert(frame.__rmmDropdownRefreshes, function() ddInfoPlace.Refresh() end)
        y = y - 72

        local showCoordsToggle = ModernCheckbox(sV, T("SHOW_COORDINATES", "Show coordinates"), T("SHOW_COORDINATES_DESC", "Append map coordinates next to the zone name"), sV, y,
            function() return rMinimapDB.showCoordinates ~= false end,
            function(on)
                rMinimapDB.showCoordinates = on
                if ns.ApplyInfoTextColorsFromDB then ns.ApplyInfoTextColorsFromDB() end
            end)
        tinsert(frame.__rmmDropdownRefreshes, function() showCoordsToggle.Refresh() end)
        y = y - 52

        local ZONE_FONT_OPTIONS = BuildZoneFontOptions()
        EnsureCurrentZoneFontInOptions(ZONE_FONT_OPTIONS, cfg)
        local ddZoneFont = ModernDropdown(sV, T("ZONE_TEXT_FONT", "Zone text font"), ZONE_FONT_OPTIONS,
            function()
                if rMinimapDB.zoneFontMediaKey and rMinimapDB.zoneFontMediaKey ~= "" then
                    return RMM_FONT_LSM .. rMinimapDB.zoneFontMediaKey
                end
                local p = rMinimapDB.zoneFontFamily or cfg.info.zoneFont.family
                return RMM_FONT_PATH .. p
            end,
            function(v)
                if type(v) ~= "string" then return end
                if v:sub(1, #RMM_FONT_LSM) == RMM_FONT_LSM then
                    rMinimapDB.zoneFontMediaKey = v:sub(#RMM_FONT_LSM + 1)
                    rMinimapDB.zoneFontFamily = nil
                elseif v:sub(1, #RMM_FONT_PATH) == RMM_FONT_PATH then
                    rMinimapDB.zoneFontFamily = v:sub(#RMM_FONT_PATH + 1)
                    rMinimapDB.zoneFontMediaKey = nil
                else
                    rMinimapDB.zoneFontFamily = v
                    rMinimapDB.zoneFontMediaKey = nil
                end
                if ns.ApplyZoneFontFromDB then ns.ApplyZoneFontFromDB() end
            end,
            sV, y, CONTENT_W - 30, nil, 16)
        tinsert(frame.__rmmDropdownRefreshes, function() ddZoneFont.Refresh() end)
        y = y - 72
        local zoneFontDesc = FS(sV, "GameFontHighlightSmall")
        zoneFontDesc:SetPoint("TOPLEFT", ddZoneFont, "BOTTOMLEFT", 0, -2)
        zoneFontDesc:SetWidth(CONTENT_W - 30)
        zoneFontDesc:SetJustifyH("LEFT")
        zoneFontDesc:SetTextColor(unpack(C.textDim))
        zoneFontDesc:SetText(T("ZONE_TEXT_FONT_DESC", ""))
        y = y - 30

        local rowZoneColor = ModernColorPickerRow(sV, T("ZONE_TEXT_COLOR", "Zone text color"), sV, y,
            function()
                local c = cfg.info.zoneFont.color
                local db = rMinimapDB.infoZoneColor
                if type(db) == "table" and tonumber(db[1]) and tonumber(db[2]) and tonumber(db[3]) then
                    return db[1], db[2], db[3]
                end
                return unpack(c)
            end,
            function(r, g, b)
                rMinimapDB.infoZoneColor = { r, g, b }
                if ns.ApplyInfoTextColorsFromDB then
                    ns.ApplyInfoTextColorsFromDB()
                end
            end)
        tinsert(frame.__rmmDropdownRefreshes, function()
            rowZoneColor.Refresh()
        end)
        y = y - 32
        local rowPerfColor = ModernColorPickerRow(sV, T("PERF_TEXT_COLOR", "Coords / ping / FPS color"), sV, y,
            function()
                local c = cfg.info.perfFont.color
                local db = rMinimapDB.infoPerfColor
                if type(db) == "table" and tonumber(db[1]) and tonumber(db[2]) and tonumber(db[3]) then
                    return db[1], db[2], db[3]
                end
                return unpack(c)
            end,
            function(r, g, b)
                rMinimapDB.infoPerfColor = { r, g, b }
                if ns.ApplyInfoTextColorsFromDB then
                    ns.ApplyInfoTextColorsFromDB()
                end
            end)
        tinsert(frame.__rmmDropdownRefreshes, function()
            rowPerfColor.Refresh()
        end)
        y = y - 32
        y = y - 22
        ModernSlider(sV, T("ZONE_TEXT_SCALE", "Zone Text Scale"), T("ZONE_TEXT_SCALE_DESC", "Scale of the zone name below the minimap"), 0.2, 2.0, 0.05, sV, y,
            function() return rMinimapDB.zoneFontScale or cfg.info.zoneFont.scale end,
            function(v)
                rMinimapDB.zoneFontScale = v
                if ns.infoZoneText then ns.infoZoneText:SetScale(v) end
            end,
            function(v) return string.format("%.0f%%", v * 100) end)
        y = y - 68
        ModernSlider(sV, T("FPS_PING_SCALE", "FPS / Ping Text Scale"), T("FPS_PING_SCALE_DESC", "Scale of the performance line (ms / fps)"), 0.2, 2.0, 0.05, sV, y,
            function() return rMinimapDB.perfFontScale or cfg.info.perfFont.scale end,
            function(v)
                rMinimapDB.perfFontScale = v
                if ns.infoPerfText then ns.infoPerfText:SetScale(v) end
            end,
            function(v) return string.format("%.0f%%", v * 100) end)
        y = y - 68
        local infoYSlider = ModernSlider(sV, T("INFO_TEXT_Y_OFFSET", "Info Text Y Offset"), T("INFO_TEXT_Y_OFFSET_DESC", "Vertical offset from the minimap edge (depends on position: below or above)"), -200, 200, 1, sV, y,
            function()
                local placement = rMinimapDB.infoTextPlacement or "below"
                if placement == "above" then
                    return rMinimapDB.infoTextYAbove or (cfg.info.posAboveY or 110)
                end
                return rMinimapDB.infoTextY or cfg.info.pos.y
            end,
            function(v)
                v = math.floor(v)
                local placement = rMinimapDB.infoTextPlacement or "below"
                if placement == "above" then
                    rMinimapDB.infoTextYAbove = v
                else
                    rMinimapDB.infoTextY = v
                end
                if ns.ApplyInfoTextLayoutFromDB then ns.ApplyInfoTextLayoutFromDB() end
            end,
            function(v) return math.floor(v) .. "px" end)
        tinsert(frame.__rmmDropdownRefreshes, function() infoYSlider.Refresh() end)
        y = y - 68

        HRule(sV, sV, y); y = y - 22
        SectionHeader(sV, T("BORDER_TEXTURE", "Border Texture"), sV, 0, y); y = y - 32
        local ddBorder = ModernDropdown(sV, T("OVERLAY", "Overlay"), OVERLAY_OPTIONS,
            function() return rMinimapDB.overlayTexture or "Interface\\AddOns\\rMinimap\\media\\map_overlay" end,
            function(v) ApplyOverlayTexture(v) end,
            sV, y, CONTENT_W - 30, "path")
        tinsert(frame.__rmmDropdownRefreshes, function() ddBorder.Refresh() end)
        y = y - 72
        HRule(sV, sV, y); y = y - 22
        SectionHeader(sV, T("COGWHEEL_RING", "Cogwheel Ring"), sV, 0, y); y = y - 32
        ModernSlider(sV, T("COGWHEEL_SCALE", "Cogwheel Scale"), T("COGWHEEL_SCALE_DESC", "Size of the rotating cogwheel texture"), 0.8, 2.0, 0.01, sV, y,
            function() return rMinimapDB.cogScale or 1.22 end,
            function(v)
                rMinimapDB.cogScale = v
                local cog = ns.cogTex
                if cog and Minimap then
                    cog:SetWidth(Minimap:GetHeight() * v)
                    cog:SetHeight(Minimap:GetHeight() * v)
                end
            end,
            function(v) return string.format("%.0f%%", v * 100) end)
        y = y - 68
        ModernSlider(sV, T("ROTATION_SPEED", "Rotation Speed"), T("ROTATION_SPEED_DESC", "Duration in seconds for a full rotation (higher = slower)"), 5, 180, 1, sV, y,
            function() return rMinimapDB.cogSpeed or 60 end,
            function(v)
                v = math.floor(v)
                rMinimapDB.cogSpeed = v
                local cog = ns.cogTex
                if cog and cog.ag and cog.ag.a1 then
                    cog.ag:Stop()
                    cog.ag.a1:SetDuration(v)
                    cog.ag:Play()
                end
            end,
            function(v) return math.floor(v) .. "s" end)
        y = y - 68
        local COG_DIR_OPTS = {
            { text = T("DIR_LEFT", "Left (counter-clockwise)"), value = "left" },
            { text = T("DIR_RIGHT", "Right (clockwise)"),        value = "right" },
        }
        local ddCogDir = ModernDropdown(sV, T("ROTATION_DIRECTION", "Rotation Direction"), COG_DIR_OPTS,
            function() return rMinimapDB.cogDirection or "left" end,
            function(v)
                rMinimapDB.cogDirection = v
                local cog = ns.cogTex
                if cog and cog.ag and cog.ag.a1 then
                    cog.ag:Stop()
                    cog.ag.a1:SetDegrees(v == "right" and 360 or -360)
                    cog.ag:Play()
                end
            end,
            sV, y, CONTENT_W - 30)
        tinsert(frame.__rmmDropdownRefreshes, function() ddCogDir.Refresh() end)
        y = y - 72
        sV:SetHeight(math.abs(y) + 40)
    end
    tabPanels[2] = panelV

    -- Tab 3: Effects
    local panelE, sE = MakeScroll(contentBg); panelE:Hide()
    do
        local y = -PAD
        SectionHeader(sE, T("FIRE_EFFECT", "Fire Effect"), sE, 0, y); y = y - 10
        local fireHint = FS(sE, "GameFontNormalSmall")
        fireHint:SetPoint("TOPLEFT", sE, "TOPLEFT", 6, y)
        fireHint:SetWidth(CONTENT_W - 40)
        fireHint:SetTextColor(0.55, 0.52, 0.48, 1)
        fireHint:SetText(T("FIRE_HINT", "Only active for map_overlay_new textures."))
        y = y - 28
        local fireShowToggle = ModernCheckbox(sE, T("SHOW_FIRE", "Show fire effect"), nil, sE, y,
            function() return rMinimapDB.portalShow ~= false end,
            function(v)
                rMinimapDB.portalShow = v
                if ns.ApplyPortalLayoutFromDB then ns.ApplyPortalLayoutFromDB() end
            end)
        tinsert(frame.__rmmDropdownRefreshes, function() fireShowToggle.Refresh() end)
        y = y - 36
        local slFireScale = ModernSlider(sE, T("FIRE_FRAME_SIZE", "Fire Frame Size"), T("FIRE_FRAME_SIZE_DESC", "Size of the holder frame relative to the minimap (outer area)"), 0.8, 4.0, 0.05, sE, y,
            function() return rMinimapDB.portalScale or 2.6 end,
            function(v)
                rMinimapDB.portalScale = v
                if ns.ApplyPortalLayoutFromDB then ns.ApplyPortalLayoutFromDB() end
            end,
            function(v) return string.format("%.0f%%", v * 100) end)
        tinsert(frame.__rmmDropdownRefreshes, function() slFireScale.Refresh() end)
        y = y - 68
        local slFireCam = ModernSlider(sE, T("FIRE_MODEL_ZOOM", "Fire Model Zoom"), T("FIRE_MODEL_ZOOM_DESC", "Camera distance of the 3D model (low = large / near, high = small / far)"), 0.2, 6.0, 0.05, sE, y,
            function() return rMinimapDB.portalCamScale or 4.45 end,
            function(v)
                rMinimapDB.portalCamScale = v
                if ns.ApplyPortalLayoutFromDB then ns.ApplyPortalLayoutFromDB() end
            end,
            function(v) return string.format("%.2f", v) end)
        tinsert(frame.__rmmDropdownRefreshes, function() slFireCam.Refresh() end)
        y = y - 68
        local slFireZ = ModernSlider(sE, T("FIRE_POS_Z", "Fire Pos Z"), T("FIRE_POS_Z_DESC", "Vertical position of the 3D model (negative = lower)"), -3.0, 3.0, 0.05, sE, y,
            function() return rMinimapDB.portalPosZ or -0.1 end,
            function(v)
                rMinimapDB.portalPosZ = v
                if ns.ApplyPortalLayoutFromDB then ns.ApplyPortalLayoutFromDB() end
            end,
            function(v) return string.format("%.2f", v) end)
        tinsert(frame.__rmmDropdownRefreshes, function() slFireZ.Refresh() end)
        y = y - 68
        local slFireAlpha = ModernSlider(sE, T("FIRE_OPACITY", "Fire Opacity"), T("FIRE_OPACITY_DESC", "Opacity of the fire effect"), 0.0, 1.0, 0.01, sE, y,
            function() return rMinimapDB.portalAlpha or 0.52 end,
            function(v)
                rMinimapDB.portalAlpha = v
                if ns.ApplyPortalLayoutFromDB then ns.ApplyPortalLayoutFromDB() end
            end,
            function(v) return string.format("%.0f%%", v * 100) end)
        tinsert(frame.__rmmDropdownRefreshes, function() slFireAlpha.Refresh() end)
        y = y - 68
        sE:SetHeight(math.abs(y) + 40)
    end
    tabPanels[3] = panelE

    -- Tab 4: About
    local panelA, sA = MakeScroll(contentBg); panelA:Hide()
    do
        local y = -PAD
        SectionHeader(sA, T("ABOUT", "About"), sA, 0, y); y = y - 28
        local cl1 = FS(sA, "GameFontNormal")
        cl1:SetPoint("TOPLEFT", sA, "TOPLEFT", 6, y)
        cl1:SetTextColor(C.primaryHi[1], C.primaryHi[2], C.primaryHi[3], 1)
        cl1:SetText(T("ABOUT_TITLE", "rMinimap - Diablo 3 Minimap"))
        y = y - 22
        local cl2 = FS(sA, "GameFontHighlightSmall")
        cl2:SetPoint("TOPLEFT", sA, "TOPLEFT", 6, y)
        cl2:SetTextColor(unpack(C.textDim))
        cl2:SetText(T("ABOUT_HINT", "Type /rmm to toggle settings"))
        y = y - 28
        local close2 = StyledButton(sA, T("CLOSE", "Close"), 120, 26, function() ConfigFadeOut(frame) end)
        close2:SetPoint("TOPLEFT", sA, "TOPLEFT", 0, y)
        y = y - 40
        sA:SetHeight(math.abs(y) + 40)
    end
    tabPanels[4] = panelA

    SelectTab(1)

    configFrame = frame
    return frame
end

-- ── public API ─────────────────────────────────────────────────────────────
function ns:ShowConfig()
    local f = BuildConfigFrame()
    if f:IsShown() then ConfigFadeOut(f) else ConfigFadeIn(f) end
end

function ns:ToggleGUI()
    ns:ShowConfig()
end

-- ── addon compartment (TOC) ───────────────────────────────────────────────
function rMinimap_OnAddonCompartmentClick(_, btn)
    if btn == "LeftButton" then
        ns:ShowConfig()
    elseif btn == "RightButton" then
        ns:ShowConfig()
    end
end

function rMinimap_OnAddonCompartmentEnter(_, tooltip)
    if not tooltip or not tooltip.AddLine then return end
    tooltip:SetText(T("AC_TITLE", "rMinimap"))
    tooltip:AddLine(T("AC_LEFT", "Left-click: Open Settings"), 1, 1, 1)
    tooltip:AddLine(T("AC_RIGHT", "Right-click: Open Settings"), 1, 1, 1)
    tooltip:Show()
end

function rMinimap_OnAddonCompartmentLeave()
    GameTooltip:Hide()
end

-- ── blizzard addon panel ───────────────────────────────────────────────────
local function RegisterBlizzardPanel()
    local panel = CreateFrame("Frame", "rMinimapInterfacePanel")
    panel.name = "rMinimap"

    local logo = panel:CreateTexture(nil, "ARTWORK")
    logo:SetTexture("Interface\\AddOns\\rMinimap\\media\\logo_big")
    logo:SetSize(381, 254)
    logo:SetPoint("TOP", panel, "TOP", 0, -28)

    local titleLbl = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalHuge")
    titleLbl:SetPoint("TOP", logo, "BOTTOM", 0, -10)
    titleLbl:SetTextColor(C.primary[1], C.primary[2], C.primary[3], 1)
    titleLbl:SetText(T("TITLE", "rMinimap"))

    local descLbl = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    descLbl:SetPoint("TOP", titleLbl, "BOTTOM", 0, -10)
    descLbl:SetWidth(440)
    descLbl:SetJustifyH("CENTER")
    descLbl:SetTextColor(unpack(C.textDim))
    descLbl:SetText(T("PANEL_DESC", "Diablo 3 themed minimap - /rmm to open settings"))

    local openBtn = StyledButton(panel, T("OPEN_SETTINGS", "Open Settings"), 240, 40, function()
        C_Timer.After(0, function()
            if SettingsPanel and SettingsPanel:IsShown() then HideUIPanel(SettingsPanel) end
            ns:ShowConfig()
        end)
    end)
    openBtn:SetPoint("TOP", descLbl, "BOTTOM", 0, -20)

    local accentL = panel:CreateTexture(nil, "ARTWORK")
    accentL:SetHeight(1); accentL:SetWidth(240)
    accentL:SetPoint("TOP", openBtn, "BOTTOM", 0, -6)
    accentL:SetGradient("HORIZONTAL",
        CreateColor(0, 0, 0, 0),
        CreateColor(C.primary[1], C.primary[2], C.primary[3], 0.6))
    local accentR = panel:CreateTexture(nil, "ARTWORK")
    accentR:SetHeight(1); accentR:SetWidth(240)
    accentR:SetPoint("TOP", openBtn, "BOTTOM", 0, -6)
    accentR:SetGradient("HORIZONTAL",
        CreateColor(C.primary[1], C.primary[2], C.primary[3], 0.6),
        CreateColor(0, 0, 0, 0))

    local cat = Settings.RegisterCanvasLayoutCategory(panel, panel.name)
    Settings.RegisterAddOnCategory(cat)
    ns.categoryID = cat:GetID()
end

-- ── re-apply saved values (safety net after reload) ─────────────────────
local function ReapplySavedValues()
    if not rMinimapDB then return end
    if ns.ApplyMinimapClockFromDB then ns.ApplyMinimapClockFromDB() end
    if rMinimapDB.overlayTexture and ns.overlayTex then
        ApplyOverlayTexture(rMinimapDB.overlayTexture)
    end
    if rMinimapDB.clusterScale and MinimapCluster then
        MinimapCluster:SetScale(rMinimapDB.clusterScale)
    end
    if rMinimapDB.zoneFontScale and ns.infoZoneText then
        ns.infoZoneText:SetScale(rMinimapDB.zoneFontScale)
    end
    if rMinimapDB.perfFontScale and ns.infoPerfText then
        ns.infoPerfText:SetScale(rMinimapDB.perfFontScale)
    end
    if ns.ApplyInfoTextLayoutFromDB then
        ns.ApplyInfoTextLayoutFromDB()
    end
    if ns.ApplyInfoTextColorsFromDB then
        ns.ApplyInfoTextColorsFromDB()
    end
    if ns.ApplyZoneFontFromDB then
        ns.ApplyZoneFontFromDB()
    end
    local cog = ns.cogTex
    if cog and Minimap then
        if rMinimapDB.cogScale then
            cog:SetWidth(Minimap:GetHeight() * rMinimapDB.cogScale)
            cog:SetHeight(Minimap:GetHeight() * rMinimapDB.cogScale)
        end
        if cog.ag and cog.ag.a1 then
            if rMinimapDB.cogDirection then
                cog.ag.a1:SetDegrees(rMinimapDB.cogDirection == "right" and 360 or -360)
            end
            if rMinimapDB.cogSpeed then
                cog.ag:Stop()
                cog.ag.a1:SetDuration(rMinimapDB.cogSpeed)
                cog.ag:Play()
            end
        end
    end
    if ns.ApplyPortalLayoutFromDB then
        ns.ApplyPortalLayoutFromDB()
    end
end

-- ── slash command ──────────────────────────────────────────────────────────
local boot = CreateFrame("Frame")
boot:RegisterEvent("PLAYER_LOGIN")
boot:SetScript("OnEvent", function(self)
    self:UnregisterEvent("PLAYER_LOGIN")

    BuildConfigFrame()

    if ns.ApplyMinimapClockFromDB then ns.ApplyMinimapClockFromDB() end

    C_Timer.After(0.5, ReapplySavedValues)
    C_Timer.After(2, ReapplySavedValues)

    RegisterBlizzardPanel()

    SLASH_RMINIMAP1 = "/rmm"
    SLASH_RMINIMAP2 = "/rminimap"
    SlashCmdList["RMINIMAP"] = function(msg)
        msg = (msg or ""):lower():match("^%s*(.-)%s*$")
        ns:ShowConfig()
    end
end)
