local addonName, ns = ...

function ns.CreateEpicBGTab()
    local UI = ns.UI

    UI.tabFrames[2] = UI.CreateTabContent()
    UI.tabButtons[2] = UI.CreateTabButton("Epic Battlegrounds", 2)

    local subFrames, subButtons, resetSubs, selectSub = UI.CreateSubTabSystem(UI.tabFrames[2], {"Enemy Bars", "Friendly Bars"})
    ns._epicBGResetSubTabs = resetSubs

    -- Every header shares the framework's plain-text build: full
    -- hairline/platform/hairline band sized from the rendered string,
    -- resting at the candlelight tint
    local DecorateTextHeader = UI.DecorateTextHeader

    -- ========================================
    -- Sub-tab 1: Enemy Bars
    -- ========================================
    -- Both declared up front so the enemy block's handlers can refresh the
    -- friendly preview when "Use Enemy Bar Settings" is mirroring it.
    local eb, fb = {}, {}

    -- Friendly mirrors enemy bar settings, so an enemy-side change has to
    -- redraw the friendly preview too or it silently goes stale.
    local function RefreshMirroredPreview()
        if fb.UpdatePreview and ns.GetEpicFriendlyUseEnemySettings() then fb.UpdatePreview() end
    end
    local ebParent = subFrames[1]

    -- Forward declaration: the Range Check toggle re-runs the grey pass
    -- from a callback built before UpdateEBGreyState is defined.
    local UpdateEBGreyState

    -- Column 1: Size settings
    eb.enableCheckbox = UI.CreateCheckbox(ebParent, 20, -20, "Enable Frames", "epicEnemyEnable", nil)
    eb.enableCheckbox:SetSize(32, 32)
    eb.enableCheckbox.text:SetFont(ns.LOCALE_FONT, 20, "")
    eb.enableCheckbox.text:SetTextColor(1, 1, 1)
    eb.enableCheckbox.text:SetShadowColor(0, 0, 0, 1)
    eb.enableCheckbox.text:SetShadowOffset(1, -1)

    -- Tier-1 master switch: larger pill marks it a rank above the section
    -- toggles (same family and artwork, just bigger); label nudged right so
    -- the wider pill doesn't crowd it. Applied BEFORE the header build so
    -- the band width accounts for the bigger pill and wider gap.
    eb.enableCheckbox.togglePill:SetScale(1.5)
    eb.enableCheckbox.text:SetPoint("LEFT", eb.enableCheckbox, "RIGHT", 16, 0)

    DecorateTextHeader(eb.enableCheckbox)
    UI.AttachHeaderHover(eb.enableCheckbox)

    -- Minor header over the size/spacing sliders, in line with Bar Options
    eb.barHeader = ebParent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    eb.barHeader:SetPoint("TOPLEFT", 20, -124)
    eb.barHeader:SetText("Bar Settings")
    eb.barHeader:SetTextColor(1, 1, 1)
    DecorateTextHeader(eb.barHeader)

    eb.widthSlider        = UI.CreateSlider(ebParent, 20, -172, "Bar Width",           100, 400, 5,  "epicEnemyBarWidth",           true)
    eb.heightSlider       = UI.CreateSlider(ebParent, 20, -252, "Bar Height",          16,  50,  1,  "epicEnemyBarHeight",          true)
    eb.spacingSlider      = UI.CreateSlider(ebParent, 20, -332, "Bar Spacing",         0,   10,  1,  "epicEnemyBarSpacing",         true)
    eb.textSizeSlider     = UI.CreateSlider(ebParent, 20, -412, "Centre Display Text Size", 8,   20,  1,  "epicEnemyTextSize",  true)
    eb.headingTextSlider  = UI.CreateSlider(ebParent, 20, -492, "Heading Text Size",   8,   20,  1,  "epicEnemyHeadingTextSize",    true)
    eb.orbStackSlider     = UI.CreateSlider(ebParent, 20, -572, "Objective Text Size", 8,   30,  1,  "epicEnemyOrbStackTextSize",   true)
    eb.rangeOpacitySlider = UI.CreateSlider(ebParent, 20, -652, "Out of Range Opacity", 0.0, 1.0, 0.05, "epicEnemyRangeOpacity", true)

    -- Column 2: Bar options
    eb.optHeader = ebParent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    eb.optHeader:SetPoint("TOPLEFT", 470, -124)
    eb.optHeader:SetText("Bar Options")
    eb.optHeader:SetTextColor(1, 1, 1)
    DecorateTextHeader(eb.optHeader)

    eb.powerBarCheckbox        = UI.CreateCheckbox(ebParent, 470, -160, "Power Bars", "epicEnemyPowerBarEnabled", function()
        eb.UpdatePreview()
        RefreshMirroredPreview()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)
    eb.textDisplaySeg = UI.CreateSegmentedToggle(ebParent, 770, -236, "Centre Text Display",
        ns.TEXT_DISPLAY_SEGMENTS,
        ns.GetEpicEnemyTextDisplayMode,
        function(v) ns.settings.epicEnemyTextDisplayMode = v end,
        function()
            eb.UpdatePreview()
            RefreshMirroredPreview()
            if not InCombatLockdown() then ns.RebuildBars() end
            ns.RefreshKeybindWidgets()   -- label says which source the key reveals
        end)
    -- For players who run Text Display on "Show Name and Spec" and want the
    -- card given over to the text rather than to artwork.
    eb.hideClassArtCheckbox = UI.CreateCheckbox(ebParent, 770, -160, "Hide Class Art", "epicEnemyHideClassArt", function()
        eb.UpdatePreview()
        RefreshMirroredPreview()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)

    eb.rangeCheckbox = UI.CreateCheckbox(ebParent, 470, -208, "Range Check", "epicEnemyRangeCheck", function()
        UpdateEBGreyState()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)

    eb.rangeInfo = ebParent:CreateFontString(nil, "OVERLAY")
    eb.rangeInfo:SetFont(ns.LOCALE_FONT, 13, "")
    eb.rangeInfo:SetPoint("TOPLEFT", eb.rangeCheckbox, "BOTTOMLEFT", 26, 4)
    eb.rangeInfo:SetJustifyH("LEFT")
    ns.RegisterRangeLabel(eb.rangeInfo)
    eb.hideFactionInfoCheckbox = UI.CreateCheckbox(ebParent, 770, -208, "Hide Faction and Player Number",          "epicEnemyHideFactionInfo", nil)

    -- Column 3: Appearance
    eb.appearHeader = ebParent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    eb.appearHeader:SetPoint("TOPLEFT", 770, -124)
    eb.appearHeader:SetText("Appearance")
    eb.appearHeader:SetTextColor(1, 1, 1)
    DecorateTextHeader(eb.appearHeader)

    -- Icon Art Style is SHARED -- one visual identity addon-wide -- so the
    -- same control is mirrored on every Appearance section. Greyed by the
    -- panel enable only, never by Hide Class Art or Use Enemy Settings:
    -- changing it here still changes every other frame set.
    eb.iconArtSeg = UI.CreateSegmentedToggle(ebParent, 770, -284, "Icon Art Style",
        ns.CLASS_ART_SEGMENTS,
        ns.GetClassArtStyle, ns.SetClassArtStyle,
        function()
            if ns.RefreshPreviews then ns.RefreshPreviews() end
            if not InCombatLockdown() then ns.RebuildBars() end
        end)

    -- Ping Assist
    eb.pingCheckbox = UI.CreateCheckbox(ebParent, 470, -460, "Enemy Ping Assist", "epicEnemyPingEnabled", function()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)
    eb.pingCheckbox.text:SetFontObject("GameFontNormalLarge")
    eb.pingCheckbox.text:SetTextColor(1, 1, 1)

    -- Clickable header: same plain-text band as every other header,
    -- with the hover cue attached
    DecorateTextHeader(eb.pingCheckbox)
    UI.AttachHeaderHover(eb.pingCheckbox)

    do
        local pingOpts = ns.PingTypeOptions(false)
        eb.pingLeftDropdown  = UI.CreateDropdown(ebParent, 470, -516, "Ping Left Click",  150, pingOpts, "epicEnemyPingLeftClick",  ns.GetEpicEnemyPingLeftClick)
        eb.pingRightDropdown = UI.CreateDropdown(ebParent, 470, -581, "Ping Right Click", 150, pingOpts, "epicEnemyPingRightClick", ns.GetEpicEnemyPingRightClick)
    end

    eb.pingZoomSlider = UI.CreateSlider(ebParent, 470, -651, "Ping Hover Zoom", 1.0, 3.0, 0.1, "epicEnemyPingZoomAmount", true)

    -- Half width: the full 280 ran under the Keybinds column beside it.

    -- The slider is anchored to both edges of its container, so resizing

    -- the container is the whole change.

    eb.pingZoomSlider:GetParent():SetWidth(140)

    -- Column 3: Preview
    do
        local PREVIEW_NAMES = { "Korak", "Thalis", "Ashon", "Nyxar", "Varen", "Fenix", "Moris", "Joral" }
        local PREVIEW_CLASSES = { "WARRIOR", "MAGE", "DRUID", "ROGUE", "PRIEST", "HUNTER", "SHAMAN", "WARLOCK" }
        local PREVIEW_SPECS = { "Arms", "Frost", "Balance", "Assassination", "Discipline", "Marksmanship", "Elemental", "Affliction" }
        local PREVIEW_HP    = { 0.85, 1.0, 0.6, 0.92, 0.45, 0.78, 0.55, 0.7 }

        eb.previewWrapper = CreateFrame("Frame", nil, ebParent)
        eb.previewWrapper:SetPoint("TOPLEFT", ebParent, "TOPLEFT", 1150, -165)
        eb.previewWrapper:SetSize(400, 500)

        -- Soft column shadow behind the cards, matching the live container.
        eb.previewShade = eb.previewWrapper:CreateTexture(nil, "BACKGROUND")
        eb.previewShade:SetTexture("Interface\\AddOns\\BlitzFramesTargets\\Textures\\ColumnShade.tga")
        eb.previewShade:SetVertexColor(0, 0, 0, 1)
        eb.previewShade:SetAlpha(0.60)

        eb.previewHeading = eb.previewWrapper:CreateFontString(nil, "OVERLAY")
        eb.previewHeading:SetFont(ns.LOCALE_FONT, 12, "")
        eb.previewHeading:SetText("Alliance Players (8)")
        eb.previewHeading:SetTextColor(1, 1, 1, 1)

        -- Same framed band the live frames use, from the same builder.
        eb.previewWrapper.headingRuleBottom = ns.MakeHeadingRule(eb.previewWrapper)
        eb.previewWrapper.headingRuleTop    = ns.MakeHeadingRule(eb.previewWrapper)
        eb.previewWrapper.headingPlate = ns.MakeHeadingPlate(eb.previewWrapper)
        eb.previewHeading:SetJustifyH("CENTER")

        eb.previewBars = {}
        for i = 1, 8 do
            -- A real bar: same widget tree, layering and styling the live
            -- frames use, minus the secure button and interaction. Previously
            -- these were hand-rolled imitations, which is what let preview and
            -- live drift apart repeatedly.
            local bar = ns.CreateBarVisual(CreateFrame("Frame", nil, eb.previewWrapper))
            ns.ApplyBarClassVisual(bar, PREVIEW_CLASSES[i], false)

            -- Ping-dock stand-in: the live ping button is a secure Button and
            -- lives in the interactive half, so the preview supplies a plain
            -- frame for the dock animation to drive.
            bar.pingBtn = CreateFrame("Frame", nil, bar)
            bar.pingBtn:SetPoint("LEFT", bar, "RIGHT", 1, 0)
            bar.pingBtn.icon = bar.pingBtn:CreateTexture(nil, "ARTWORK")
            bar.pingBtn.icon:SetAllPoints()
            bar.pingBtn.icon:SetTexture("Interface\\AddOns\\BlitzFramesTargets\\Textures\\AssistPingV2.tga")
            bar.pingBtn:Hide()

            bar.assignedName = PREVIEW_NAMES[i]
            bar.hpFraction   = PREVIEW_HP[i]
            eb.previewBars[i] = bar
        end

        ns.AttachPingDock({
            bars = eb.previewBars, maxBars = 8,
            getZoom = function() return ns.G.epicEnemy.pingZoom() end,
        })

        -- Card dock on its own set table: it keeps driver state, so it
        -- must not share the ping dock's.
        ns.AttachCardDock({ bars = eb.previewBars, maxBars = 8 })

        -- Objective demos drive each bar's own icon slot — the same widgets and
        -- the same appliers the live frames use — instead of building a second
        -- set of frames on top. That also drops the +10 frame-level workaround
        -- the hand-rolled frames needed to climb over the nested bar layers.
        ns.ApplyObjectiveIcon(eb.previewBars[4], 10)  -- purple orb, animated
        eb.previewBars[4].pvpIconStackText:SetText("120")
        eb.previewBars[4].pvpIconStackText:Show()
        ns.ApplyObjectiveIcon(eb.previewBars[8], 0)   -- horde banner, animated
        -- Flag stacks are a shared match CLOCK -- 1, 2, 3... -- not the orb's
        -- damage percentage, so they are usually a single digit. Both are shown
        -- because a one-digit number and a three-digit one sit very differently
        -- in the slot, and only the orb's was ever previewed.
        eb.previewBars[8].pvpIconStackText:SetText("4")
        eb.previewBars[8].pvpIconStackText:Show()
        -- Deephaul crystal. Included because 100 is the one classification the
        -- addon INVENTS rather than mirrors from Enum.PvPUnitClassification, so it
        -- is the one most easily missed when the icon or glow tables change.
        ns.ApplyObjectiveIcon(eb.previewBars[2], 100)
        eb.previewBars[2].pvpIconStackText:SetText("7")
        eb.previewBars[2].pvpIconStackText:Show()

        eb.previewBars[6].previewStatus = "Dead"
        eb.previewBars[6].hpFraction    = 0
        eb.previewBars[7].previewStatus = "Stealth"

        function eb.UpdatePreview()
            local barW = ns.G.epicEnemy.barWidth()
            local barH = ns.G.epicEnemy.barHeight()
            local barS = ns.G.epicEnemy.barSpacing()
            local textS = ns.G.epicEnemy.textSize()
            local headS = ns.G.epicEnemy.headingSize()
            local orbS  = ns.G.epicEnemy.orbStackSize()
            local hideFaction = ns.G.epicEnemy.hideFaction()

            -- Heading sits above the wrapper, not inside it, matching live where
            -- it is anchored BOTTOM to the container TOP and reserves no space.
            if hideFaction then
                eb.previewHeading:Hide()
                ns.ShowHeadingRules(eb.previewWrapper, false)
            else
                ns.ShowHeadingRules(eb.previewWrapper, true)
                eb.previewHeading:Show()
                eb.previewHeading:SetFont(ns.LOCALE_FONT, headS, "")
                -- Enemy previews show Horde and friendly Alliance, matching test
                -- mode, so the two faction hazes can be compared side by side.
                eb.previewHeading:SetText(ns.BuildFactionHeading(0, 8, headS))
                -- Same shift the live heading takes: with the class art hanging
                -- OUTSIDE the card the band centres on the whole composition, not
                -- on the bare card. Derived from barH rather than from a bar --
                -- this runs BEFORE the bar loop below, so no bar is laid out yet.
                local hShift = -(ns.ArtOutsideWidth(barH, ns.G.epicEnemy.hideClassArt()) / 2)
                ns.LayOutHeadingBand(eb.previewWrapper, eb.previewHeading, headS, hShift)
            end

            local totalH = 8 * (barH + barS) - barS
            eb.previewWrapper:SetSize(barW, totalH)
            eb.previewWrapper:SetPoint("TOPLEFT", ebParent, "TOPLEFT", 1150, -165)

            eb.previewShade:ClearAllPoints()
            eb.previewShade:SetPoint("TOPLEFT", eb.previewWrapper, "TOPLEFT", -14, 14)
            eb.previewShade:SetPoint("BOTTOMRIGHT", eb.previewWrapper, "BOTTOMRIGHT", 14, -14)

            local powerEnabled = ns.G.epicEnemy.powerEnabled()
            local pingEnabled = ns.G.epicEnemy.pingEnabled()
                        for i, bar in ipairs(eb.previewBars) do
                bar:SetSize(barW, barH)
                bar:ClearAllPoints()
                bar:SetPoint("TOPLEFT", eb.previewWrapper, "TOPLEFT", 0, -((i - 1) * (barH + barS)))

                -- Identical geometry to live, from the same function.
                ns.ApplyBarLayout(bar, barW, barH, textS, powerEnabled, ns.G.epicEnemy.hideClassArt())

                -- Status suppresses the card exactly as live does: dead
                -- empties and dims; stealth keeps a normal card under the fog.
                local suppressed = bar.previewStatus ~= nil
                -- A frozen respawn countdown, so the preview shows where the
                -- timer sits and how it is styled without ticking.
                ns.SetDeadTimer(bar, bar.previewStatus == "Dead" and 12 or nil, ns.G.epicEnemy.orbStackSize())
                ns.ApplyBarClassVisual(bar, PREVIEW_CLASSES[i], suppressed, bar.previewStatus == "Stealth")
                ns.ApplyBarStateArt(bar, bar.previewStatus, nil)
                bar.healthBar:SetMinMaxValues(0, 1)
                -- Dead empties; stealth holds its value under the fog (matches
                -- RenderEnemyHealth rule 1 since 2026-08-19)
                bar.healthBar:SetValue(bar.previewStatus == "Dead" and 0 or bar.hpFraction)

                -- Same composer the live bars use, fed literal mock values, so
                -- the preview can't drift from real behaviour.
                ns.ComposeLabel(bar, ns.G.epicEnemy.textDisplay(), PREVIEW_NAMES[i], PREVIEW_SPECS[i], bar.previewStatus)
                ns.PlaceBarLabel(bar, bar.healthBar)

                if powerEnabled then
                    bar.powerBar:SetMinMaxValues(0, 1)
                    bar.powerBar:SetValue(suppressed and 0 or 0.7)
                    bar.powerBar:SetStatusBarColor(ns.GetClassPowerColor(PREVIEW_CLASSES[i]))
                end

                bar.pingBtn:SetSize(barH - 2, barH - 2)
                if pingEnabled then bar.pingBtn:Show() else bar.pingBtn:Hide() end
            end

            eb.previewBars[4].pvpIconStackText:SetFont(ns.LOCALE_FONT, orbS, "OUTLINE")
        end

        eb.UpdatePreview()

        eb.heightSlider:HookScript("OnValueChanged",       function() eb.UpdatePreview(); RefreshMirroredPreview() end)
        eb.widthSlider:HookScript("OnValueChanged",        function() eb.UpdatePreview(); RefreshMirroredPreview() end)
        eb.spacingSlider:HookScript("OnValueChanged",      function() eb.UpdatePreview(); RefreshMirroredPreview() end)
        eb.textSizeSlider:HookScript("OnValueChanged",     function() eb.UpdatePreview(); RefreshMirroredPreview() end)
        eb.headingTextSlider:HookScript("OnValueChanged",  function() eb.UpdatePreview(); RefreshMirroredPreview() end)
        eb.orbStackSlider:HookScript("OnValueChanged",     function() eb.UpdatePreview(); RefreshMirroredPreview() end)
        -- Grey state re-asserted on every SHOW, not just on panel open.
        -- The panel refreshes all four tabs while their content frames are
        -- still hidden, and something in that ordering left the first open
        -- ungreyed often enough to be reported. Deriving it again at the
        -- moment the content is actually visible closes the whole class:
        -- grey state comes from settings, so re-deriving it is always safe
        -- and FadeTo either animates it or hands it to the reveal cascade.
        ebParent:HookScript("OnShow", function()
            eb.UpdatePreview()
            UpdateEBGreyState()
        end)
    end

    eb.moveNote = UI.CreateMoveNote(ebParent, eb.previewWrapper)

    -- Anchored BELOW the preview rather than at a fixed y: the preview grows
    -- with the Bar Height slider (16-50) and eight bars at 50 would swallow
    -- anything placed at a constant offset.
    eb.keybindHeader = ebParent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    eb.keybindHeader:SetPoint("TOPLEFT", 770, -468)
    eb.keybindHeader:SetText("Keybinds")
    eb.keybindHeader:SetTextColor(1, 1, 1)
    DecorateTextHeader(eb.keybindHeader)

    -- Every instance is interactive. One binding sits behind all four, but a
    -- greyed mirror read as broken — visible and untouchable.
    eb.peekKeybind = UI.CreateKeybindWidget(ebParent, eb.keybindHeader, ns.GetEpicEnemyTextDisplayMode)

    -- Not a binding: the gesture is the wheel itself, so it is a plain on/off.
    eb.wheelStatus = UI.CreateWheelToggleWidget(ebParent, eb.peekKeybind)

    -- Grey state
    function UpdateEBGreyState()
        local en = eb.enableCheckbox:GetChecked()
        UI.SetSliderGreyState(eb.heightSlider, en)
        UI.SetSliderGreyState(eb.widthSlider, en)
        UI.SetSliderGreyState(eb.spacingSlider, en)
        UI.SetSliderGreyState(eb.headingTextSlider, en)
        UI.SetSliderGreyState(eb.textSizeSlider, en)
        UI.SetSliderGreyState(eb.orbStackSlider, en)
        UI.SetHeaderGreyState(eb.barHeader, en)
        UI.SetHeaderGreyState(eb.optHeader, en)
        UI.FadeTo(eb.previewWrapper, en and 1 or UI.GREY_ALPHA)
        UI.FadeTo(eb.moveNote, en and 1 or UI.GREY_ALPHA)
        UI.SetGreyState(eb.powerBarCheckbox, en)
        UI.SetHeaderGreyState(eb.appearHeader, en)
        UI.SetGreyState(eb.textDisplaySeg, en)
        UI.SetGreyState(eb.iconArtSeg, en)
        UI.SetGreyState(eb.hideFactionInfoCheckbox, en)
        UI.SetGreyState(eb.hideClassArtCheckbox, en)
        UI.SetGreyState(eb.rangeCheckbox, en)
        local ebRange = en and eb.rangeCheckbox:GetChecked()
        UI.FadeTo(eb.rangeInfo, ebRange and 1 or UI.GREY_ALPHA)
        UI.SetSliderGreyState(eb.rangeOpacitySlider, ebRange)
        UI.SetGreyState(eb.pingCheckbox, en)
        local pingActive = en and eb.pingCheckbox:GetChecked()
        UI.SetDropdownGreyState(eb.pingLeftDropdown, pingActive)
        UI.SetDropdownGreyState(eb.pingRightDropdown, pingActive)
        UI.SetSliderGreyState(eb.pingZoomSlider, pingActive)
    end

    eb.enableCheckbox:HookScript("OnClick", UpdateEBGreyState)
    eb.pingCheckbox:HookScript("OnClick", UpdateEBGreyState)
    eb.pingCheckbox:HookScript("OnClick", function() eb.UpdatePreview() end)
    eb.hideFactionInfoCheckbox:HookScript("OnClick", function() eb.UpdatePreview(); RefreshMirroredPreview() end)

    -- ========================================
    -- Sub-tab 2: Friendly Bars
    -- ========================================
    local fbParent = subFrames[2]

    -- Forward declaration: the Range Check toggle re-runs the grey pass
    -- from a callback built before UpdateFBGreyState is defined.
    local UpdateFBGreyState

    -- Column 1: Size settings
    fb.enableCheckbox = UI.CreateCheckbox(fbParent, 20, -20, "Enable Frames", "epicFriendlyEnable", nil)
    fb.enableCheckbox:SetSize(32, 32)
    fb.enableCheckbox.text:SetFont(ns.LOCALE_FONT, 20, "")
    fb.enableCheckbox.text:SetTextColor(1, 1, 1)
    fb.enableCheckbox.text:SetShadowColor(0, 0, 0, 1)
    fb.enableCheckbox.text:SetShadowOffset(1, -1)

    -- Tier-1 master switch (see enemy sub-tab); sized before the header
    -- build so the band encapsulates the bigger pill
    fb.enableCheckbox.togglePill:SetScale(1.5)
    fb.enableCheckbox.text:SetPoint("LEFT", fb.enableCheckbox, "RIGHT", 16, 0)

    DecorateTextHeader(fb.enableCheckbox)
    UI.AttachHeaderHover(fb.enableCheckbox)

    -- Minor header over the size/spacing sliders, in line with Bar Options
    fb.barHeader = fbParent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    fb.barHeader:SetPoint("TOPLEFT", 20, -124)
    fb.barHeader:SetText("Bar Settings")
    fb.barHeader:SetTextColor(1, 1, 1)
    DecorateTextHeader(fb.barHeader)

    fb.widthSlider        = UI.CreateSlider(fbParent, 20, -172, "Bar Width",           100, 400, 5,  "epicFriendlyBarWidth",           true)
    fb.heightSlider       = UI.CreateSlider(fbParent, 20, -252, "Bar Height",          16,  50,  1,  "epicFriendlyBarHeight",          true)
    fb.spacingSlider      = UI.CreateSlider(fbParent, 20, -332, "Bar Spacing",         0,   10,  1,  "epicFriendlyBarSpacing",         true)
    fb.textSizeSlider     = UI.CreateSlider(fbParent, 20, -412, "Centre Display Text Size", 8,   20,  1,  "epicFriendlyTextSize",  true)
    fb.headingTextSlider  = UI.CreateSlider(fbParent, 20, -492, "Heading Text Size",   8,   20,  1,  "epicFriendlyHeadingTextSize",    true)
    fb.orbStackSlider     = UI.CreateSlider(fbParent, 20, -572, "Objective Text Size", 8,   30,  1,  "epicFriendlyOrbStackTextSize",   true)
    fb.rangeOpacitySlider = UI.CreateSlider(fbParent, 20, -652, "Out of Range Opacity", 0.0, 1.0, 0.05, "epicFriendlyRangeOpacity", true)

    fb.useEnemyCheckbox = UI.CreateCheckbox(fbParent, 470, -20, "Use Enemy Bar Settings", "epicFriendlyUseEnemySettings", function()
        fb.UpdatePreview()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)
    fb.useEnemyCheckbox:SetSize(32, 32)
    fb.useEnemyCheckbox.text:SetFont(ns.LOCALE_FONT, 20, "")
    fb.useEnemyCheckbox.text:SetTextColor(1, 1, 1)
    fb.useEnemyCheckbox.text:SetShadowColor(0, 0, 0, 1)
    fb.useEnemyCheckbox.text:SetShadowOffset(1, -1)

    -- Tier-2 section toggle: full band + hover, standard pill
    DecorateTextHeader(fb.useEnemyCheckbox)
    UI.AttachHeaderHover(fb.useEnemyCheckbox)

    -- Column 2: Bar options
    fb.optHeader = fbParent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    fb.optHeader:SetPoint("TOPLEFT", 470, -124)
    fb.optHeader:SetText("Bar Options")
    fb.optHeader:SetTextColor(1, 1, 1)
    DecorateTextHeader(fb.optHeader)

    fb.powerBarCheckbox        = UI.CreateCheckbox(fbParent, 470, -160, "Power Bars", "epicFriendlyPowerBarEnabled", function()
        fb.UpdatePreview()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)
    fb.textDisplaySeg = UI.CreateSegmentedToggle(fbParent, 770, -236, "Centre Text Display",
        ns.TEXT_DISPLAY_SEGMENTS,
        ns.GetEpicFriendlyTextDisplayMode,
        function(v) ns.settings.epicFriendlyTextDisplayMode = v end,
        function()
            fb.UpdatePreview()
            if not InCombatLockdown() then ns.RebuildBars() end
            ns.RefreshKeybindWidgets()   -- label says which source the key reveals
        end)
    -- For players who run Text Display on "Show Name and Spec" and want the
    -- card given over to the text rather than to artwork.
    fb.hideClassArtCheckbox = UI.CreateCheckbox(fbParent, 770, -160, "Hide Class Art", "epicFriendlyHideClassArt", function()
        fb.UpdatePreview()
        RefreshMirroredPreview()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)

    fb.rangeCheckbox = UI.CreateCheckbox(fbParent, 470, -208, "Range Check", "epicFriendlyRangeCheck", function()
        UpdateFBGreyState()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)

    fb.rangeInfo = fbParent:CreateFontString(nil, "OVERLAY")
    fb.rangeInfo:SetFont(ns.LOCALE_FONT, 13, "")
    fb.rangeInfo:SetPoint("TOPLEFT", fb.rangeCheckbox, "BOTTOMLEFT", 26, 4)
    fb.rangeInfo:SetJustifyH("LEFT")
    -- Friendly range comes from UnitInRange, so there is no spell to name.
    fb.rangeInfo:SetText("Distance ~40 (group range)")
    fb.rangeInfo:SetTextColor(unpack(ns.Palette.textLabel))
    fb.hideFactionInfoCheckbox = UI.CreateCheckbox(fbParent, 770, -208, "Hide Faction and Player Number",          "epicFriendlyHideFactionInfo", nil)

    -- Column 3: Appearance
    fb.appearHeader = fbParent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    fb.appearHeader:SetPoint("TOPLEFT", 770, -124)
    fb.appearHeader:SetText("Appearance")
    fb.appearHeader:SetTextColor(1, 1, 1)
    DecorateTextHeader(fb.appearHeader)

    -- Icon Art Style is SHARED -- one visual identity addon-wide -- so the
    -- same control is mirrored on every Appearance section. Greyed by the
    -- panel enable only, never by Hide Class Art or Use Enemy Settings:
    -- changing it here still changes every other frame set.
    fb.iconArtSeg = UI.CreateSegmentedToggle(fbParent, 770, -284, "Icon Art Style",
        ns.CLASS_ART_SEGMENTS,
        ns.GetClassArtStyle, ns.SetClassArtStyle,
        function()
            if ns.RefreshPreviews then ns.RefreshPreviews() end
            if not InCombatLockdown() then ns.RebuildBars() end
        end)

    -- Ping Assist
    fb.pingCheckbox = UI.CreateCheckbox(fbParent, 470, -460, "Friendly Ping Assist", "epicFriendlyPingEnabled", function()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)
    fb.pingCheckbox.text:SetFontObject("GameFontNormalLarge")
    fb.pingCheckbox.text:SetTextColor(1, 1, 1)

    -- Clickable header: same plain-text band as every other header,
    -- with the hover cue attached
    DecorateTextHeader(fb.pingCheckbox)
    UI.AttachHeaderHover(fb.pingCheckbox)

    do
        local friendlyPingOpts = ns.PingTypeOptions(true)
        fb.pingLeftDropdown  = UI.CreateDropdown(fbParent, 470, -516, "Ping Left Click",  150, friendlyPingOpts, "epicFriendlyPingLeftClick",  ns.GetEpicFriendlyPingLeftClick)
        fb.pingRightDropdown = UI.CreateDropdown(fbParent, 470, -581, "Ping Right Click", 150, friendlyPingOpts, "epicFriendlyPingRightClick", ns.GetEpicFriendlyPingRightClick)
    end

    fb.pingZoomSlider = UI.CreateSlider(fbParent, 470, -651, "Ping Hover Zoom", 1.0, 3.0, 0.1, "epicFriendlyPingZoomAmount", true)

    -- Half width: the full 280 ran under the Keybinds column beside it.

    -- The slider is anchored to both edges of its container, so resizing

    -- the container is the whole change.

    fb.pingZoomSlider:GetParent():SetWidth(140)

    -- Column 3: Preview
    do
        local PREVIEW_NAMES = { "Korak", "Thalis", "Ashon", "Nyxar", "Varen", "Fenix", "Moris", "Joral" }
        local PREVIEW_CLASSES = { "WARRIOR", "MAGE", "DRUID", "ROGUE", "PRIEST", "HUNTER", "SHAMAN", "WARLOCK" }
        local PREVIEW_SPECS = { "Arms", "Frost", "Balance", "Assassination", "Discipline", "Marksmanship", "Elemental", "Affliction" }
        local PREVIEW_HP    = { 0.85, 1.0, 0.6, 0.92, 0.45, 0.78, 0.55, 0.7 }

        fb.previewWrapper = CreateFrame("Frame", nil, fbParent)
        fb.previewWrapper:SetPoint("TOPLEFT", fbParent, "TOPLEFT", 1150, -165)
        fb.previewWrapper:SetSize(400, 500)

        -- Soft column shadow behind the cards, matching the live container.
        fb.previewShade = fb.previewWrapper:CreateTexture(nil, "BACKGROUND")
        fb.previewShade:SetTexture("Interface\\AddOns\\BlitzFramesTargets\\Textures\\ColumnShade.tga")
        fb.previewShade:SetVertexColor(0, 0, 0, 1)
        fb.previewShade:SetAlpha(0.60)

        fb.previewHeading = fb.previewWrapper:CreateFontString(nil, "OVERLAY")
        fb.previewHeading:SetFont(ns.LOCALE_FONT, 12, "")
        fb.previewHeading:SetText("Alliance Players (8)")
        fb.previewHeading:SetTextColor(1, 1, 1, 1)

        -- Same framed band the live frames use, from the same builder.
        fb.previewWrapper.headingRuleBottom = ns.MakeHeadingRule(fb.previewWrapper)
        fb.previewWrapper.headingRuleTop    = ns.MakeHeadingRule(fb.previewWrapper)
        fb.previewWrapper.headingPlate = ns.MakeHeadingPlate(fb.previewWrapper)
        fb.previewHeading:SetJustifyH("CENTER")

        fb.previewBars = {}
        for i = 1, 8 do
            -- A real bar: same widget tree, layering and styling the live
            -- frames use, minus the secure button and interaction. Previously
            -- these were hand-rolled imitations, which is what let preview and
            -- live drift apart repeatedly.
            local bar = ns.CreateBarVisual(CreateFrame("Frame", nil, fb.previewWrapper))
            ns.ApplyBarClassVisual(bar, PREVIEW_CLASSES[i], false)

            -- Ping-dock stand-in: the live ping button is a secure Button and
            -- lives in the interactive half, so the preview supplies a plain
            -- frame for the dock animation to drive.
            bar.pingBtn = CreateFrame("Frame", nil, bar)
            bar.pingBtn:SetPoint("LEFT", bar, "RIGHT", 1, 0)
            bar.pingBtn.icon = bar.pingBtn:CreateTexture(nil, "ARTWORK")
            bar.pingBtn.icon:SetAllPoints()
            bar.pingBtn.icon:SetTexture("Interface\\AddOns\\BlitzFramesTargets\\Textures\\AssistPingV2.tga")
            bar.pingBtn:Hide()

            bar.assignedName = PREVIEW_NAMES[i]
            bar.hpFraction   = PREVIEW_HP[i]
            fb.previewBars[i] = bar
        end

        ns.AttachPingDock({
            bars = fb.previewBars, maxBars = 8,
            getZoom = function() return ns.G.epicFriendly.pingZoom() end,
        })

        -- Card dock on its own set table: it keeps driver state, so it
        -- must not share the ping dock's.
        ns.AttachCardDock({ bars = fb.previewBars, maxBars = 8 })

        -- Objective demos drive each bar's own icon slot — the same widgets and
        -- the same appliers the live frames use — instead of building a second
        -- set of frames on top. That also drops the +10 frame-level workaround
        -- the hand-rolled frames needed to climb over the nested bar layers.
        ns.ApplyObjectiveIcon(fb.previewBars[1], 10)  -- purple orb, animated
        fb.previewBars[1].pvpIconStackText:SetText("120")
        fb.previewBars[1].pvpIconStackText:Show()
        ns.ApplyObjectiveIcon(fb.previewBars[8], 0)   -- horde banner, animated
        -- Flag stacks are a shared match CLOCK -- 1, 2, 3... -- not the orb's
        -- damage percentage, so they are usually a single digit. Both are shown
        -- because a one-digit number and a three-digit one sit very differently
        -- in the slot, and only the orb's was ever previewed.
        fb.previewBars[8].pvpIconStackText:SetText("4")
        fb.previewBars[8].pvpIconStackText:Show()
        -- Deephaul crystal. Included because 100 is the one classification the
        -- addon INVENTS rather than mirrors from Enum.PvPUnitClassification, so it
        -- is the one most easily missed when the icon or glow tables change.
        ns.ApplyObjectiveIcon(fb.previewBars[2], 100)
        fb.previewBars[2].pvpIconStackText:SetText("7")
        fb.previewBars[2].pvpIconStackText:Show()

        fb.previewBars[6].previewStatus = "Dead"
        fb.previewBars[6].hpFraction    = 0


        function fb.UpdatePreview()
            local barW = ns.G.epicFriendly.barWidth()
            local barH = ns.G.epicFriendly.barHeight()
            local barS = ns.G.epicFriendly.barSpacing()
            local textS = ns.G.epicFriendly.textSize()
            local headS = ns.G.epicFriendly.headingSize()
            local orbS  = ns.G.epicFriendly.orbStackSize()
            local hideFaction = ns.G.epicFriendly.hideFaction()

            -- Heading sits above the wrapper, not inside it, matching live where
            -- it is anchored BOTTOM to the container TOP and reserves no space.
            if hideFaction then
                fb.previewHeading:Hide()
                ns.ShowHeadingRules(fb.previewWrapper, false)
            else
                ns.ShowHeadingRules(fb.previewWrapper, true)
                fb.previewHeading:Show()
                fb.previewHeading:SetFont(ns.LOCALE_FONT, headS, "")
                -- Enemy previews show Horde and friendly Alliance, matching test
                -- mode, so the two faction hazes can be compared side by side.
                fb.previewHeading:SetText(ns.BuildFactionHeading(1, 8, headS))
                -- Same shift the live heading takes: with the class art hanging
                -- OUTSIDE the card the band centres on the whole composition, not
                -- on the bare card. Derived from barH rather than from a bar --
                -- this runs BEFORE the bar loop below, so no bar is laid out yet.
                local hShift = -(ns.ArtOutsideWidth(barH, ns.G.epicFriendly.hideClassArt()) / 2)
                ns.LayOutHeadingBand(fb.previewWrapper, fb.previewHeading, headS, hShift)
            end

            local totalH = 8 * (barH + barS) - barS
            fb.previewWrapper:SetSize(barW, totalH)
            fb.previewWrapper:SetPoint("TOPLEFT", fbParent, "TOPLEFT", 1150, -165)

            fb.previewShade:ClearAllPoints()
            fb.previewShade:SetPoint("TOPLEFT", fb.previewWrapper, "TOPLEFT", -14, 14)
            fb.previewShade:SetPoint("BOTTOMRIGHT", fb.previewWrapper, "BOTTOMRIGHT", 14, -14)

            local powerEnabled = ns.G.epicFriendly.powerEnabled()
            local pingEnabled = ns.G.epicFriendly.pingEnabled()
                        for i, bar in ipairs(fb.previewBars) do
                bar:SetSize(barW, barH)
                bar:ClearAllPoints()
                bar:SetPoint("TOPLEFT", fb.previewWrapper, "TOPLEFT", 0, -((i - 1) * (barH + barS)))

                -- Identical geometry to live, from the same function.
                ns.ApplyBarLayout(bar, barW, barH, textS, powerEnabled, ns.G.epicFriendly.hideClassArt())

                -- Status suppresses the card exactly as live does: dead
                -- empties and dims; stealth keeps a normal card under the fog.
                local suppressed = bar.previewStatus ~= nil
                -- A frozen respawn countdown, so the preview shows where the
                -- timer sits and how it is styled without ticking.
                ns.SetDeadTimer(bar, bar.previewStatus == "Dead" and 12 or nil, ns.G.epicFriendly.orbStackSize())
                ns.ApplyBarClassVisual(bar, PREVIEW_CLASSES[i], suppressed, bar.previewStatus == "Stealth")
                ns.ApplyBarStateArt(bar, bar.previewStatus, nil)
                bar.healthBar:SetMinMaxValues(0, 1)
                -- Dead empties; stealth holds its value under the fog (matches
                -- RenderEnemyHealth rule 1 since 2026-08-19)
                bar.healthBar:SetValue(bar.previewStatus == "Dead" and 0 or bar.hpFraction)

                -- Same composer the live bars use, fed literal mock values, so
                -- the preview can't drift from real behaviour.
                ns.ComposeLabel(bar, ns.G.epicFriendly.textDisplay(), PREVIEW_NAMES[i], PREVIEW_SPECS[i], bar.previewStatus)
                ns.PlaceBarLabel(bar, bar.healthBar)

                if powerEnabled then
                    bar.powerBar:SetMinMaxValues(0, 1)
                    bar.powerBar:SetValue(suppressed and 0 or 0.7)
                    bar.powerBar:SetStatusBarColor(ns.GetClassPowerColor(PREVIEW_CLASSES[i]))
                end

                bar.pingBtn:SetSize(barH - 2, barH - 2)
                if pingEnabled then bar.pingBtn:Show() else bar.pingBtn:Hide() end
            end

            fb.previewBars[1].pvpIconStackText:SetFont(ns.LOCALE_FONT, orbS, "OUTLINE")
        end

        fb.UpdatePreview()

        fb.heightSlider:HookScript("OnValueChanged",       function() fb.UpdatePreview() end)
        fb.widthSlider:HookScript("OnValueChanged",        function() fb.UpdatePreview() end)
        fb.spacingSlider:HookScript("OnValueChanged",      function() fb.UpdatePreview() end)
        fb.textSizeSlider:HookScript("OnValueChanged",     function() fb.UpdatePreview() end)
        fb.headingTextSlider:HookScript("OnValueChanged",  function() fb.UpdatePreview() end)
        fb.orbStackSlider:HookScript("OnValueChanged",     function() fb.UpdatePreview() end)
        -- Grey state re-asserted on every SHOW, not just on panel open.
        -- The panel refreshes all four tabs while their content frames are
        -- still hidden, and something in that ordering left the first open
        -- ungreyed often enough to be reported. Deriving it again at the
        -- moment the content is actually visible closes the whole class:
        -- grey state comes from settings, so re-deriving it is always safe
        -- and FadeTo either animates it or hands it to the reveal cascade.
        fbParent:HookScript("OnShow", function()
            fb.UpdatePreview()
            UpdateFBGreyState()
        end)
    end

    fb.moveNote = UI.CreateMoveNote(fbParent, fb.previewWrapper)

    -- Anchored BELOW the preview rather than at a fixed y: the preview grows
    -- with the Bar Height slider (16-50) and eight bars at 50 would swallow
    -- anything placed at a constant offset.
    fb.keybindHeader = fbParent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    fb.keybindHeader:SetPoint("TOPLEFT", 770, -468)
    fb.keybindHeader:SetText("Keybinds")
    fb.keybindHeader:SetTextColor(1, 1, 1)
    DecorateTextHeader(fb.keybindHeader)

    -- Every instance is interactive. One binding sits behind all four, but a
    -- greyed mirror read as broken — visible and untouchable.
    fb.peekKeybind = UI.CreateKeybindWidget(fbParent, fb.keybindHeader, ns.GetEpicFriendlyTextDisplayMode)

    -- Not a binding: the gesture is the wheel itself, so it is a plain on/off.
    fb.wheelStatus = UI.CreateWheelToggleWidget(fbParent, fb.peekKeybind)

    -- Grey state
    function UpdateFBGreyState()
        local en      = fb.enableCheckbox:GetChecked()
        local mirror  = fb.useEnemyCheckbox:GetChecked()
        local sliders = en and not mirror
        UI.SetSliderGreyState(fb.heightSlider, sliders)
        UI.SetSliderGreyState(fb.widthSlider, sliders)
        UI.SetSliderGreyState(fb.spacingSlider, sliders)
        UI.SetSliderGreyState(fb.headingTextSlider, sliders)
        UI.SetSliderGreyState(fb.textSizeSlider, sliders)
        UI.SetSliderGreyState(fb.orbStackSlider, sliders)
        UI.SetHeaderGreyState(fb.barHeader, sliders)
        UI.SetHeaderGreyState(fb.optHeader, sliders)
        UI.FadeTo(fb.previewWrapper, en and 1 or UI.GREY_ALPHA)
        UI.FadeTo(fb.moveNote, en and 1 or UI.GREY_ALPHA)
        UI.SetGreyState(fb.powerBarCheckbox, en and not mirror)
        UI.SetHeaderGreyState(fb.appearHeader, en)
        UI.SetGreyState(fb.textDisplaySeg, en and not mirror)
        UI.SetGreyState(fb.iconArtSeg, en)
        UI.SetGreyState(fb.hideFactionInfoCheckbox, en and not mirror)
        UI.SetGreyState(fb.hideClassArtCheckbox, en and not mirror)
        UI.SetGreyState(fb.rangeCheckbox, en and not mirror)
        local fbRange = en and not mirror and fb.rangeCheckbox:GetChecked()
        UI.FadeTo(fb.rangeInfo, fbRange and 1 or UI.GREY_ALPHA)
        UI.SetSliderGreyState(fb.rangeOpacitySlider, fbRange)
        UI.SetGreyState(fb.useEnemyCheckbox, en)
        UI.SetGreyState(fb.pingCheckbox, en)
        local pingActive = en and fb.pingCheckbox:GetChecked()
        UI.SetDropdownGreyState(fb.pingLeftDropdown, pingActive)
        UI.SetDropdownGreyState(fb.pingRightDropdown, pingActive)
        UI.SetSliderGreyState(fb.pingZoomSlider, pingActive)
    end

    fb.enableCheckbox:HookScript("OnClick", UpdateFBGreyState)
    fb.useEnemyCheckbox:HookScript("OnClick", UpdateFBGreyState)
    fb.useEnemyCheckbox:HookScript("OnClick", function() fb.UpdatePreview() end)
    fb.pingCheckbox:HookScript("OnClick", UpdateFBGreyState)
    fb.pingCheckbox:HookScript("OnClick", function() fb.UpdatePreview() end)
    fb.hideFactionInfoCheckbox:HookScript("OnClick", function() fb.UpdatePreview() end)

    -- ========================================
    -- OnShow: sync all widgets from settings
    -- ========================================
    -- The peek key drives live bars, test bars AND these previews. The
    -- previews are not in frameSets and no ticker touches them, so they
    -- register a refresher rather than being found by the render loop.
    ns.RegisterPreviewRefresh(eb.UpdatePreview)
    ns.RegisterPreviewRefresh(fb.UpdatePreview)

    return function()
        if not ns.settings then return end

        -- Enemy Bars
        eb.enableCheckbox:SetChecked(ns.GetEpicEnemyEnable())
        eb.widthSlider:SetValue(ns.settings.epicEnemyBarWidth              or ns.defaults.epicEnemyBarWidth)
        eb.heightSlider:SetValue(ns.settings.epicEnemyBarHeight             or ns.defaults.epicEnemyBarHeight)
        eb.spacingSlider:SetValue(ns.settings.epicEnemyBarSpacing           or ns.defaults.epicEnemyBarSpacing)
        eb.textSizeSlider:SetValue(ns.settings.epicEnemyTextSize            or ns.defaults.epicEnemyTextSize)
        eb.headingTextSlider:SetValue(ns.settings.epicEnemyHeadingTextSize  or ns.defaults.epicEnemyHeadingTextSize)
        eb.orbStackSlider:SetValue(ns.settings.epicEnemyOrbStackTextSize    or ns.defaults.epicEnemyOrbStackTextSize)
        eb.powerBarCheckbox:SetChecked(ns.GetEpicEnemyPowerBarEnabled())
        eb.textDisplaySeg:SetValue(ns.GetEpicEnemyTextDisplayMode(), true)
        eb.iconArtSeg:SetValue(ns.GetClassArtStyle(), true)
        eb.hideFactionInfoCheckbox:SetChecked(ns.GetEpicEnemyHideFactionInfo())
        eb.hideClassArtCheckbox:SetChecked(ns.GetEpicEnemyHideClassArt())
        eb.rangeCheckbox:SetChecked(ns.GetEpicEnemyRangeCheck())
        eb.rangeOpacitySlider:SetValue(ns.settings.epicEnemyRangeOpacity or ns.defaults.epicEnemyRangeOpacity)
        eb.pingCheckbox:SetChecked(ns.GetEpicEnemyPingEnabled())
        eb.pingLeftDropdown:GenerateMenu()
        eb.pingRightDropdown:GenerateMenu()
        eb.pingZoomSlider:SetValue(ns.settings.epicEnemyPingZoomAmount or ns.defaults.epicEnemyPingZoomAmount)
        eb.UpdatePreview()
        UpdateEBGreyState()

        -- Friendly Bars
        fb.enableCheckbox:SetChecked(ns.GetEpicFriendlyEnable())
        fb.useEnemyCheckbox:SetChecked(ns.GetEpicFriendlyUseEnemySettings())
        fb.widthSlider:SetValue(ns.settings.epicFriendlyBarWidth              or ns.defaults.epicFriendlyBarWidth)
        fb.heightSlider:SetValue(ns.settings.epicFriendlyBarHeight             or ns.defaults.epicFriendlyBarHeight)
        fb.spacingSlider:SetValue(ns.settings.epicFriendlyBarSpacing           or ns.defaults.epicFriendlyBarSpacing)
        fb.textSizeSlider:SetValue(ns.settings.epicFriendlyTextSize            or ns.defaults.epicFriendlyTextSize)
        fb.headingTextSlider:SetValue(ns.settings.epicFriendlyHeadingTextSize  or ns.defaults.epicFriendlyHeadingTextSize)
        fb.orbStackSlider:SetValue(ns.settings.epicFriendlyOrbStackTextSize    or ns.defaults.epicFriendlyOrbStackTextSize)
        fb.powerBarCheckbox:SetChecked(ns.GetEpicFriendlyPowerBarEnabled())
        fb.textDisplaySeg:SetValue(ns.GetEpicFriendlyTextDisplayMode(), true)
        fb.iconArtSeg:SetValue(ns.GetClassArtStyle(), true)
        fb.hideFactionInfoCheckbox:SetChecked(ns.GetEpicFriendlyHideFactionInfo())
        fb.hideClassArtCheckbox:SetChecked(ns.GetEpicFriendlyHideClassArt())
        fb.rangeCheckbox:SetChecked(ns.GetEpicFriendlyRangeCheck())
        fb.rangeOpacitySlider:SetValue(ns.settings.epicFriendlyRangeOpacity or ns.defaults.epicFriendlyRangeOpacity)
        fb.pingCheckbox:SetChecked(ns.GetEpicFriendlyPingEnabled())
        fb.pingLeftDropdown:GenerateMenu()
        fb.pingRightDropdown:GenerateMenu()
        fb.pingZoomSlider:SetValue(ns.settings.epicFriendlyPingZoomAmount or ns.defaults.epicFriendlyPingZoomAmount)
        fb.UpdatePreview()
        UpdateFBGreyState()
    end
end
