local addonName, ns = ...

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

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

    local subFrames, subButtons, resetSubs, selectSub = UI.CreateSubTabSystem(UI.tabFrames[1], {"Enemy Bars", "Friendly Bars"})
    ns._bgResetSubTabs = 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.GetNormalFriendlyUseEnemySettings() then fb.UpdatePreview() end
    end
    local ebParent = subFrames[1]

    -- Forward declaration: the Range Check toggle re-runs the grey pass from
    -- its callback, and that callback is built long before UpdateEBGreyState
    -- is defined. Declared here so the closure captures the local rather than
    -- a nil global of the same name.
    local UpdateEBGreyState

    -- Column 1: Size settings
    eb.enableCheckbox = UI.CreateCheckbox(ebParent, 20, -20, "Enable Frames", "normalEnemyEnable", 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,  "normalEnemyBarWidth",           true)
    eb.heightSlider       = UI.CreateSlider(ebParent, 20, -252, "Bar Height",          16,  50,  1,  "normalEnemyBarHeight",          true)
    eb.spacingSlider      = UI.CreateSlider(ebParent, 20, -332, "Bar Spacing",         0,   10,  1,  "normalEnemyBarSpacing",         true)
    eb.textSizeSlider     = UI.CreateSlider(ebParent, 20, -412, "Centre Display Text Size", 8,   20,  1,  "normalEnemyTextSize",  true)
    eb.headingTextSlider  = UI.CreateSlider(ebParent, 20, -492, "Heading Text Size",   8,   20,  1,  "normalEnemyHeadingTextSize",    true)
    eb.orbStackSlider     = UI.CreateSlider(ebParent, 20, -572, "Objective Text Size", 8,   30,  1,  "normalEnemyOrbStackTextSize",   true)
    -- Sits with the other bar sliders rather than beside its toggle: it is a
    -- bar appearance value, and greying it is the clearest way to show the
    -- whole feature is off.
    eb.rangeOpacitySlider = UI.CreateSlider(ebParent, 20, -652, "Out of Range Opacity", 0.0, 1.0, 0.05, "normalEnemyRangeOpacity", true)
    -- Guarded: CreateSlider and the OnShow restore both drive SetValue, and the
    -- first of those runs long before eb.UpdatePreview is defined further down.
    eb.rangeOpacitySlider:HookScript("OnValueChanged", function()
        if eb.UpdatePreview then eb.UpdatePreview() end
    end)

    -- 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", "normalEnemyPowerBarEnabled", function()
        eb.UpdatePreview()
        RefreshMirroredPreview()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)
    eb.textDisplaySeg = UI.CreateSegmentedToggle(ebParent, 770, -250, "Centre Text Display",
        ns.TEXT_DISPLAY_SEGMENTS,
        ns.GetNormalEnemyTextDisplayMode,
        function(v) ns.settings.normalEnemyTextDisplayMode = 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", "normalEnemyHideClassArt", function()
        eb.UpdatePreview()
        RefreshMirroredPreview()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)

    -- Normal BGs only. Epic has no equivalent toggle: forty players means a
    -- dozen can share one target and the row stops meaning anything.
    eb.teamTargetCheckbox = UI.CreateCheckbox(ebParent, 470, -270, "Show Team Targeting", "normalEnemyTeamTargeting", function()
        eb.UpdatePreview()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)

    -- Sits directly under Team Targeting because it borrows that slot: the
    -- reveal is drawn where the grid would be, then hands it back.
    eb.stealthRevealCheckbox = UI.CreateCheckbox(ebParent, 470, -318, "Stealth Reveal", "normalEnemyStealthReveal", function()
        eb.UpdatePreview()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)

    -- No legacy addon does this, so the label cannot lean on anything the user
    -- already knows.
    UI.AttachHelpTip(eb.stealthRevealCheckbox, "Stealth Reveal",
        "Shows the team member (excluding you) who first targets an enemy coming out of stealth, for 6 seconds before fading out.")

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

    -- Not a dropdown: there is nothing to choose. The probe spell is whatever
    -- the current spec actually owns, so the panel reports it rather than
    -- offering it. Text is rebuilt whenever the spell re-resolves.
    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.ResolveRangeSpell()
    ns.RegisterRangeLabel(eb.rangeInfo)

    eb.hideFactionInfoCheckbox = UI.CreateCheckbox(ebParent, 770, -208, "Hide Faction and Player Number",        "normalEnemyHideFactionInfo", 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, -298, "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, -430, "Enemy Ping Assist", "normalEnemyPingEnabled", 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, -486, "Ping Left Click",  150, pingOpts, "normalEnemyPingLeftClick",  ns.GetNormalEnemyPingLeftClick)
        eb.pingRightDropdown = UI.CreateDropdown(ebParent, 470, -551, "Ping Right Click", 150, pingOpts, "normalEnemyPingRightClick", ns.GetNormalEnemyPingRightClick)
    end

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

    -- Column 3: Preview
    do
        local PREVIEW_NAMES = { "Korak", "Thalis", "Ashon", "Nyxar", "Varen", "Fenix", "Moris", "Joral" }
        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)

        -- Soft column shade behind the cards (matches test mode; repositioned
        -- below the heading in UpdatePreview — the heading stays outside it)
        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.previewWrapper:SetSize(400, 500)

        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")

        -- Modern-look prototype (enemy preview only; friendly keeps the old
        -- style for comparison): custom gradient fill tinted with desaturated
        -- class colours + class art bleeding into the bar from the left
        local PREVIEW_CLASSES = { "WARRIOR", "MAGE", "DRUID", "ROGUE", "PRIEST", "HUNTER", "SHAMAN", "WARLOCK" }
        local PREVIEW_SPECS = { "Arms", "Frost", "Balance", "Assassination", "Discipline", "Marksmanship", "Elemental", "Affliction" }

        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.normalEnemy.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.normalEnemy.barWidth()
            local barH = ns.G.normalEnemy.barHeight()
            local barS = ns.G.normalEnemy.barSpacing()
            local textS = ns.G.normalEnemy.textSize()
            local headS = ns.G.normalEnemy.headingSize()
            local orbS  = ns.G.normalEnemy.orbStackSize()
            local hideFaction = ns.G.normalEnemy.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))
                ns.LayOutHeadingBand(eb.previewWrapper, eb.previewHeading, headS)
            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.normalEnemy.powerEnabled()
            local pingEnabled = ns.G.normalEnemy.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.normalEnemy.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.normalEnemy.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.normalEnemy.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

                -- Same renderer live bars use, fed the shared mock.
                ns.ApplyRangeFade(bar,
                    ns.G.normalEnemy.rangeCheck() and ns.MockOutOfRange(i),
                    ns.G.normalEnemy.rangeOpacity())

                local who, cls = ns.MockStealthReveal(i)
                if ns.G.normalEnemy.stealthReveal() and ns.DrawStealthReveal(bar, who, cls, 1) then
                    ns.RenderTeamTargeting(bar, nil, nil)
                else
                    ns.HideStealthReveal(bar)
                    if ns.G.normalEnemy.teamTargeting() then
                        ns.RenderTeamTargeting(bar, ns.MockTeamTargeting(i))
                    else
                        ns.RenderTeamTargeting(bar, nil, nil)
                    end
                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)
        ebParent:HookScript("OnShow", function() eb.UpdatePreview() 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, -438)
    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.GetNormalEnemyTextDisplayMode)

    -- 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.teamTargetCheckbox, en)
        UI.SetGreyState(eb.stealthRevealCheckbox, en)
        UI.SetGreyState(eb.rangeCheckbox, en)
        local rangeActive = en and eb.rangeCheckbox:GetChecked()
        UI.FadeTo(eb.rangeInfo, rangeActive and 1 or UI.GREY_ALPHA)
        UI.SetSliderGreyState(eb.rangeOpacitySlider, rangeActive)
        UI.SetGreyState(eb.pingCheckbox, en)
        local pingActive = en and eb.pingCheckbox:GetChecked()
        UI.FadeTo(eb.pingLeftDropdown:GetParent(), pingActive and 1 or UI.GREY_ALPHA)
        UI.FadeTo(eb.pingRightDropdown:GetParent(), pingActive and 1 or UI.GREY_ALPHA)
        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 long before UpdateFBGreyState is defined.
    local UpdateFBGreyState

    -- Column 1: Size settings
    fb.enableCheckbox = UI.CreateCheckbox(fbParent, 20, -20, "Enable Frames", "normalFriendlyEnable", 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,  "normalFriendlyBarWidth",           true)
    fb.heightSlider       = UI.CreateSlider(fbParent, 20, -252, "Bar Height",          16,  50,  1,  "normalFriendlyBarHeight",          true)
    fb.spacingSlider      = UI.CreateSlider(fbParent, 20, -332, "Bar Spacing",         0,   10,  1,  "normalFriendlyBarSpacing",         true)
    fb.textSizeSlider     = UI.CreateSlider(fbParent, 20, -412, "Centre Display Text Size", 8,   20,  1,  "normalFriendlyTextSize",  true)
    fb.headingTextSlider  = UI.CreateSlider(fbParent, 20, -492, "Heading Text Size",   8,   20,  1,  "normalFriendlyHeadingTextSize",    true)
    fb.orbStackSlider     = UI.CreateSlider(fbParent, 20, -572, "Objective Text Size", 8,   30,  1,  "normalFriendlyOrbStackTextSize",   true)
    fb.rangeOpacitySlider = UI.CreateSlider(fbParent, 20, -652, "Out of Range Opacity", 0.0, 1.0, 0.05, "normalFriendlyRangeOpacity", true)

    fb.useEnemyCheckbox = UI.CreateCheckbox(fbParent, 470, -20, "Use Enemy Bar Settings", "normalFriendlyUseEnemySettings", 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", "normalFriendlyPowerBarEnabled", function()
        fb.UpdatePreview()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)
    fb.textDisplaySeg = UI.CreateSegmentedToggle(fbParent, 770, -250, "Centre Text Display",
        ns.TEXT_DISPLAY_SEGMENTS,
        ns.GetNormalFriendlyTextDisplayMode,
        function(v) ns.settings.normalFriendlyTextDisplayMode = 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", "normalFriendlyHideClassArt", function()
        fb.UpdatePreview()
        RefreshMirroredPreview()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)

    -- Normal BGs only. Epic has no equivalent toggle: forty players means a
    -- dozen can share one target and the row stops meaning anything.
    fb.rangeCheckbox = UI.CreateCheckbox(fbParent, 470, -208, "Range Check", "normalFriendlyRangeCheck", function()
        UpdateFBGreyState()
        if not InCombatLockdown() then ns.RebuildBars() end
    end)

    -- Static, unlike the enemy readout: friendly range comes from UnitInRange,
    -- so there is no spell to name and no per-spec distance to report.
    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")
    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",          "normalFriendlyHideFactionInfo", 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, -298, "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, -430, "Friendly Ping Assist", "normalFriendlyPingEnabled", 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, -486, "Ping Left Click",  150, friendlyPingOpts, "normalFriendlyPingLeftClick",  ns.GetNormalFriendlyPingLeftClick)
        fb.pingRightDropdown = UI.CreateDropdown(fbParent, 470, -551, "Ping Right Click", 150, friendlyPingOpts, "normalFriendlyPingRightClick", ns.GetNormalFriendlyPingRightClick)
    end

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

    -- 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.normalFriendly.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[4], 10)  -- purple orb, animated
        fb.previewBars[4].pvpIconStackText:SetText("120")
        fb.previewBars[4].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.normalFriendly.barWidth()
            local barH = ns.G.normalFriendly.barHeight()
            local barS = ns.G.normalFriendly.barSpacing()
            local textS = ns.G.normalFriendly.textSize()
            local headS = ns.G.normalFriendly.headingSize()
            local orbS  = ns.G.normalFriendly.orbStackSize()
            local hideFaction = ns.G.normalFriendly.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))
                ns.LayOutHeadingBand(fb.previewWrapper, fb.previewHeading, headS)
            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.normalFriendly.powerEnabled()
            local pingEnabled = ns.G.normalFriendly.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.normalFriendly.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.normalFriendly.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.normalFriendly.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

                -- Cleared, not rendered: team targeting is an enemy-only
                -- readout ("who on OUR side is on this enemy"), so the friendly
                -- pane no longer offers it.
                ns.RenderTeamTargeting(bar, nil, nil)
                bar.pingBtn:SetSize(barH - 2, barH - 2)
                if pingEnabled then bar.pingBtn:Show() else bar.pingBtn:Hide() end
            end

            fb.previewBars[4].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)
        fbParent:HookScript("OnShow", function() fb.UpdatePreview() 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, -438)
    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.GetNormalFriendlyTextDisplayMode)

    -- 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 rangeActive = en and not mirror and fb.rangeCheckbox:GetChecked()
        UI.FadeTo(fb.rangeInfo, rangeActive and 1 or UI.GREY_ALPHA)
        UI.SetSliderGreyState(fb.rangeOpacitySlider, rangeActive)
        UI.SetGreyState(fb.useEnemyCheckbox, en)
        UI.SetGreyState(fb.pingCheckbox, en)
        local pingActive = en and fb.pingCheckbox:GetChecked()
        UI.FadeTo(fb.pingLeftDropdown:GetParent(), pingActive and 1 or UI.GREY_ALPHA)
        UI.FadeTo(fb.pingRightDropdown:GetParent(), pingActive and 1 or UI.GREY_ALPHA)
        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.GetNormalEnemyEnable())
        eb.widthSlider:SetValue(ns.settings.normalEnemyBarWidth              or ns.defaults.normalEnemyBarWidth)
        eb.heightSlider:SetValue(ns.settings.normalEnemyBarHeight             or ns.defaults.normalEnemyBarHeight)
        eb.spacingSlider:SetValue(ns.settings.normalEnemyBarSpacing           or ns.defaults.normalEnemyBarSpacing)
        eb.textSizeSlider:SetValue(ns.settings.normalEnemyTextSize            or ns.defaults.normalEnemyTextSize)
        eb.headingTextSlider:SetValue(ns.settings.normalEnemyHeadingTextSize  or ns.defaults.normalEnemyHeadingTextSize)
        eb.orbStackSlider:SetValue(ns.settings.normalEnemyOrbStackTextSize    or ns.defaults.normalEnemyOrbStackTextSize)
        eb.powerBarCheckbox:SetChecked(ns.GetNormalEnemyPowerBarEnabled())
        eb.textDisplaySeg:SetValue(ns.GetNormalEnemyTextDisplayMode(), true)
        eb.iconArtSeg:SetValue(ns.GetClassArtStyle(), true)
        eb.hideFactionInfoCheckbox:SetChecked(ns.GetNormalEnemyHideFactionInfo())
        eb.hideClassArtCheckbox:SetChecked(ns.GetNormalEnemyHideClassArt())
        eb.teamTargetCheckbox:SetChecked(ns.GetNormalEnemyTeamTargeting())
        eb.stealthRevealCheckbox:SetChecked(ns.GetNormalEnemyStealthReveal())
        eb.rangeCheckbox:SetChecked(ns.GetNormalEnemyRangeCheck())
        eb.rangeOpacitySlider:SetValue(ns.settings.normalEnemyRangeOpacity or ns.defaults.normalEnemyRangeOpacity)
        eb.pingCheckbox:SetChecked(ns.GetNormalEnemyPingEnabled())
        eb.pingLeftDropdown:GenerateMenu()
        eb.pingRightDropdown:GenerateMenu()
        eb.pingZoomSlider:SetValue(ns.settings.normalEnemyPingZoomAmount or ns.defaults.normalEnemyPingZoomAmount)
        eb.UpdatePreview()
        UpdateEBGreyState()

        -- Friendly Bars
        fb.enableCheckbox:SetChecked(ns.GetNormalFriendlyEnable())
        fb.useEnemyCheckbox:SetChecked(ns.GetNormalFriendlyUseEnemySettings())
        fb.widthSlider:SetValue(ns.settings.normalFriendlyBarWidth              or ns.defaults.normalFriendlyBarWidth)
        fb.heightSlider:SetValue(ns.settings.normalFriendlyBarHeight             or ns.defaults.normalFriendlyBarHeight)
        fb.spacingSlider:SetValue(ns.settings.normalFriendlyBarSpacing           or ns.defaults.normalFriendlyBarSpacing)
        fb.textSizeSlider:SetValue(ns.settings.normalFriendlyTextSize            or ns.defaults.normalFriendlyTextSize)
        fb.headingTextSlider:SetValue(ns.settings.normalFriendlyHeadingTextSize  or ns.defaults.normalFriendlyHeadingTextSize)
        fb.orbStackSlider:SetValue(ns.settings.normalFriendlyOrbStackTextSize    or ns.defaults.normalFriendlyOrbStackTextSize)
        fb.powerBarCheckbox:SetChecked(ns.GetNormalFriendlyPowerBarEnabled())
        fb.textDisplaySeg:SetValue(ns.GetNormalFriendlyTextDisplayMode(), true)
        fb.iconArtSeg:SetValue(ns.GetClassArtStyle(), true)
        fb.hideFactionInfoCheckbox:SetChecked(ns.GetNormalFriendlyHideFactionInfo())
        fb.hideClassArtCheckbox:SetChecked(ns.GetNormalFriendlyHideClassArt())
        fb.rangeCheckbox:SetChecked(ns.GetNormalFriendlyRangeCheck())
        fb.rangeOpacitySlider:SetValue(ns.settings.normalFriendlyRangeOpacity or ns.defaults.normalFriendlyRangeOpacity)
        fb.pingCheckbox:SetChecked(ns.GetNormalFriendlyPingEnabled())
        fb.pingLeftDropdown:GenerateMenu()
        fb.pingRightDropdown:GenerateMenu()
        fb.pingZoomSlider:SetValue(ns.settings.normalFriendlyPingZoomAmount or ns.defaults.normalFriendlyPingZoomAmount)
        fb.UpdatePreview()
        UpdateFBGreyState()
    end
end
