-- Portal Authority — core/onboarding.lua
-- D0-D5c onboarding, welcome surfaces, spotlight, and move-hint marker runtime.
--
-- Depends on: PortalAuthority; PortalAuthority.CoreInternal helpers
-- Exposes:    onboarding/welcome public methods; PortalAuthority.CoreInternal.PA_PrintPortalAuthorityMessage
-- DB keys:    onboarding/profile meta keys
-- Events:     none
-- Search key: none

local ADDON_NAME = ...

PortalAuthority = PortalAuthority or {}
PortalAuthority.CoreInternal = PortalAuthority.CoreInternal or {}
local CoreInternal = PortalAuthority.CoreInternal
local L = PortalAuthority.L

local PA_IsUiSurfaceGateEnabled = CoreInternal.PA_IsUiSurfaceGateEnabled
local PA_IsStaticBaselineGateEnabled = CoreInternal.PA_IsStaticBaselineGateEnabled
local PA_IsSettingsBaselineGateEnabled = CoreInternal.PA_IsSettingsBaselineGateEnabled
local PA_IsSettingsEagerBuildGateEnabled = CoreInternal.PA_IsSettingsEagerBuildGateEnabled
local PA_IsSettingsDirectBuildGateEnabled = CoreInternal.PA_IsSettingsDirectBuildGateEnabled
local PA_IsSettingsWindowHostEnabled = CoreInternal.PA_IsSettingsWindowHostEnabled
local PA_IsSettingsWindowDevGateEnabled = CoreInternal.PA_IsSettingsWindowDevGateEnabled
local PA_IsSettingsWindowPublicGateEnabled = CoreInternal.PA_IsSettingsWindowPublicGateEnabled
local PA_IsOnboardingD1DevGateEnabled = CoreInternal.PA_IsOnboardingD1DevGateEnabled
local PA_IsOnboardingD3DevGateEnabled = CoreInternal.PA_IsOnboardingD3DevGateEnabled
local PA_IsOnboardingD4DevGateEnabled = CoreInternal.PA_IsOnboardingD4DevGateEnabled
local PA_IsOnboardingD3PublicGateEnabled = CoreInternal.PA_IsOnboardingD3PublicGateEnabled
local PA_IsOnboardingD4PublicGateEnabled = CoreInternal.PA_IsOnboardingD4PublicGateEnabled
local PA_IsOnboardingQualifyingUpgradePublicGateEnabled = CoreInternal.PA_IsOnboardingQualifyingUpgradePublicGateEnabled
local PA_IsOnboardingD3Enabled = CoreInternal.PA_IsOnboardingD3Enabled
local PA_IsOnboardingD4Enabled = CoreInternal.PA_IsOnboardingD4Enabled
local trim = CoreInternal.trim
local clampNumber = CoreInternal.clampNumber
local PA_Clamp = CoreInternal.PA_Clamp
local PA_ToNumber = CoreInternal.PA_ToNumber
local PA_SafeBool = CoreInternal.PA_SafeBool
local PA_CompareSemver = CoreInternal.PA_CompareSemver
local PA_IsSemverStringValid = CoreInternal.PA_IsSemverStringValid
local PA_GetCurrentAddonVersionState = CoreInternal.PA_GetCurrentAddonVersionState
local VALID_OUTLINES = CoreInternal.VALID_OUTLINES
local VALID_SOUND_CHANNELS = CoreInternal.VALID_SOUND_CHANNELS
local BUILTIN_FONT_FALLBACK = CoreInternal.BUILTIN_FONT_FALLBACK
local PLAYER_LOGIN_MESSAGE = CoreInternal.PLAYER_LOGIN_MESSAGE
local DOCK_ENABLE_DEFAULTS_VERSION = CoreInternal.DOCK_ENABLE_DEFAULTS_VERSION
local PA_PrintPortalAuthorityMessage = CoreInternal.PA_PrintPortalAuthorityMessage
local PA_MaybeShowMPlusNotice = CoreInternal.PA_MaybeShowMPlusNotice
local ResetMPlusNoticeShown = CoreInternal.ResetMPlusNoticeShown
local PA_IsSecretValue = CoreInternal.PA_IsSecretValue
local PA_NormalizeSafeString = CoreInternal.PA_NormalizeSafeString
local PA_SafeStringsEqual = CoreInternal.PA_SafeStringsEqual
local PA_SafeUnitGUID = CoreInternal.PA_SafeUnitGUID

local function PA_PrintPortalAuthorityMessage(message)
    print("|cffE88BC7Portal Authority:|r " .. tostring(message or ""))
end

local function PA_IsPremiumWelcomeVisible(frame)
    return frame and frame.IsShown and frame:IsShown()
end

local function PA_GetOnboardingActiveLocaleCode()
    local localeCode = trim(tostring((PortalAuthority and PortalAuthority._activeLocaleCode) or ((GetLocale and GetLocale()) or "enUS")))
    if localeCode == "" then
        localeCode = "enUS"
    end
    return localeCode
end

local function PA_IsOnboardingCjkLocale(localeCode)
    return localeCode == "zhCN" or localeCode == "zhTW" or localeCode == "koKR"
end

local function PA_ApplyOnboardingPremiumFont(authority, object, fallbackSize)
    if not authority or not object or not object.GetFont or type(authority.ApplyPremiumAuthoredFont) ~= "function" then
        return
    end
    local _, fontSize, fontFlags = object:GetFont()
    authority:ApplyPremiumAuthoredFont(object, fontSize or fallbackSize or 12, fontFlags or "")
end

local function PA_ApplyOnboardingLocalizedTextFont(authority, object, fallbackSize)
    if not authority or not object or not object.GetFont then
        return
    end

    local _, fontSize, fontFlags = object:GetFont()
    local resolvedSize = fontSize or fallbackSize or 12
    local resolvedFlags = fontFlags or ""

    if PA_IsOnboardingCjkLocale(PA_GetOnboardingActiveLocaleCode()) and type(authority.ApplyGlobalFont) == "function" then
        authority:ApplyGlobalFont(object, resolvedSize, resolvedFlags)
        return
    end

    if type(authority.ApplyPremiumAuthoredFont) == "function" then
        authority:ApplyPremiumAuthoredFont(object, resolvedSize, resolvedFlags)
    end
end

local function PA_EnsureSpecialFrame(name)
    if type(name) ~= "string" or name == "" or type(UISpecialFrames) ~= "table" then
        return
    end
    for i = 1, #UISpecialFrames do
        if UISpecialFrames[i] == name then
            return
        end
    end
    table.insert(UISpecialFrames, name)
end

local function PA_StopFrameAnimation(owner)
    if owner and owner.SetScript then
        owner:SetScript("OnUpdate", nil)
    end
end

local function PA_EaseInOut(progress)
    progress = math.max(0, math.min(1, progress or 0))
    return progress * progress * (3 - (2 * progress))
end

local function PA_StartAlphaTween(owner, tokenHolder, tokenField, fromAlpha, toAlpha, duration, onFinished)
    if not owner or not tokenHolder then
        return
    end

    tokenHolder[tokenField] = (tonumber(tokenHolder[tokenField]) or 0) + 1
    local token = tokenHolder[tokenField]
    local elapsed = 0

    owner:SetAlpha(fromAlpha)
    owner:SetScript("OnUpdate", function(self, delta)
        if tokenHolder[tokenField] ~= token then
            self:SetScript("OnUpdate", nil)
            return
        end

        elapsed = elapsed + (delta or 0)
        local progress = duration > 0 and PA_EaseInOut(elapsed / duration) or 1
        self:SetAlpha(fromAlpha + ((toAlpha - fromAlpha) * progress))
        if progress >= 1 then
            self:SetAlpha(toAlpha)
            self:SetScript("OnUpdate", nil)
            if onFinished then
                onFinished(self)
            end
        end
    end)
end

local function PA_FocusOverclockLinkPopup(popup)
    if not popup or not popup.editBox then
        return
    end
    popup.editBox:SetText(PortalAuthority._buildConfig.release.overclockWarcraftLogsUrl)
    popup.editBox:SetCursorPosition(0)
    popup.editBox:SetFocus()
    popup.editBox:HighlightText()
end

function PortalAuthority:EnsureOverclockLinkPopupFrame()
    local popup = self._overclockLinkPopupFrame
    if popup then
        return popup
    end

    popup = CreateFrame("Frame", "PortalAuthorityOverclockLinkFrame", UIParent, "BackdropTemplate")
    self._overclockLinkPopupFrame = popup
    popup:SetSize(420, 152)
    popup:SetPoint("CENTER", UIParent, "CENTER", 0, 12)
    popup:SetFrameStrata("DIALOG")
    popup:SetFrameLevel(140)
    popup:SetToplevel(true)
    popup:SetClampedToScreen(true)
    popup:EnableMouse(true)
    popup:SetMovable(false)
    popup:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    popup:SetBackdropColor(0.06, 0.07, 0.09, 0.97)
    popup:SetBackdropBorderColor(0.26, 0.29, 0.34, 1.0)
    popup:Hide()
    if popup.GetName and popup:GetName() then
        table.insert(UISpecialFrames, popup:GetName())
    end
    popup:SetScript("OnMouseDown", function() end)

    local title = popup:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    title:SetPoint("TOP", popup, "TOP", 0, -14)
    title:SetTextColor(1.0, 0.82, 0.0, 1.0)
    title:SetText(L["RUNTIME_ONBOARDING_LINK_POPUP_TITLE"])
    PA_ApplyOnboardingPremiumFont(self, title, 13)
    popup._title = title

    local helper = popup:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    helper:SetPoint("TOP", title, "BOTTOM", 0, -6)
    helper:SetTextColor(0.74, 0.74, 0.78, 1.0)
    helper:SetText(L["RUNTIME_ONBOARDING_LINK_POPUP_HELP"])
    PA_ApplyOnboardingLocalizedTextFont(self, helper, 11)
    popup._helper = helper

    local editBackdrop = CreateFrame("Frame", nil, popup, "BackdropTemplate")
    editBackdrop:SetPoint("TOPLEFT", popup, "TOPLEFT", 20, -52)
    editBackdrop:SetPoint("TOPRIGHT", popup, "TOPRIGHT", -20, -52)
    editBackdrop:SetHeight(30)
    editBackdrop:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    editBackdrop:SetBackdropColor(0.10, 0.11, 0.14, 0.96)
    editBackdrop:SetBackdropBorderColor(0.24, 0.27, 0.32, 1.0)
    popup._editBackdrop = editBackdrop

    local editBox = CreateFrame("EditBox", nil, popup, "InputBoxTemplate")
    editBox:SetAutoFocus(false)
    editBox:SetMultiLine(false)
    editBox:SetMaxLetters(255)
    editBox:SetPoint("TOPLEFT", editBackdrop, "TOPLEFT", 6, -5)
    editBox:SetPoint("BOTTOMRIGHT", editBackdrop, "BOTTOMRIGHT", -6, 5)
    editBox:SetTextInsets(0, 0, 0, 0)
    editBox:SetTextColor(0.95, 0.95, 0.97, 1.0)
    PA_ApplyOnboardingPremiumFont(self, editBox, 12)
    editBox:SetScript("OnEscapePressed", function(selfEdit)
        local owner = selfEdit and selfEdit._ownerPopup or nil
        if owner and owner.Hide then
            owner:Hide()
        end
    end)
    editBox:SetScript("OnEnterPressed", function(selfEdit)
        selfEdit:HighlightText()
    end)
    editBox:SetScript("OnEditFocusGained", function(selfEdit)
        selfEdit:HighlightText()
    end)
    editBox._ownerPopup = popup
    popup.editBox = editBox

    local closeButton = CreateFrame("Button", nil, popup, "UIPanelButtonTemplate")
    closeButton:SetSize(96, 22)
    closeButton:SetPoint("BOTTOM", popup, "BOTTOM", 0, 14)
    closeButton:SetText(L["COMMON_CLOSE"])
    closeButton:SetScript("OnClick", function()
        popup:Hide()
    end)
    PA_ApplyOnboardingLocalizedTextFont(self, closeButton.Text or (closeButton.GetFontString and closeButton:GetFontString()) or nil, 11)
    popup._closeButton = closeButton

    popup:SetScript("OnShow", function(frame)
        PA_FocusOverclockLinkPopup(frame)
        if C_Timer and C_Timer.After then
            C_Timer.After(0, function()
                if frame and frame:IsShown() then
                    PA_FocusOverclockLinkPopup(frame)
                end
            end)
        end
    end)

    popup:SetScript("OnHide", function(frame)
        if frame and frame.editBox then
            frame.editBox:ClearFocus()
        end
    end)

    return popup
end

function PortalAuthority:ShowOverclockLinkPopup()
    local popup = self:EnsureOverclockLinkPopupFrame()
    if not popup then
        return false
    end
    popup:Show()
    if popup.Raise then
        popup:Raise()
    end
    PA_FocusOverclockLinkPopup(popup)
    return true
end

function PortalAuthority:HideQualifyingUpgradeAnnouncementPopup()
    return self:_HideQualifyingUpgradeAnnouncementPopupNonConsuming("manual-hide")
end

function PortalAuthority:_IsQualifyingUpgradeAnnouncementPopupTeardownSuppressed()
    return self._qualifyingUpgradeAnnouncementTeardownSuppressed == true
end

function PortalAuthority:_IsQualifyingUpgradeAnnouncementPopupNonConsumingHideSuppressed()
    return (tonumber(self._qualifyingUpgradeAnnouncementNonConsumingHideDepth) or 0) > 0
end

function PortalAuthority:_WithSuppressedQualifyingUpgradeAnnouncementPopupFinalize(callback)
    if type(callback) ~= "function" then
        return false, "Popup suppression callback is unavailable."
    end

    self._qualifyingUpgradeAnnouncementNonConsumingHideDepth =
        (tonumber(self._qualifyingUpgradeAnnouncementNonConsumingHideDepth) or 0) + 1

    local results = { pcall(callback) }

    self._qualifyingUpgradeAnnouncementNonConsumingHideDepth =
        math.max((tonumber(self._qualifyingUpgradeAnnouncementNonConsumingHideDepth) or 1) - 1, 0)

    local ok = table.remove(results, 1)
    if not ok then
        error(results[1])
    end
    return unpack(results)
end

function PortalAuthority:_HideQualifyingUpgradeAnnouncementPopupNonConsuming(reason)
    local popup = self._qualifyingUpgradeAnnouncementPopupFrame
    if not PA_IsPremiumWelcomeVisible(popup) then
        return false
    end

    local ok, err = self:_WithSuppressedQualifyingUpgradeAnnouncementPopupFinalize(function()
        popup._paAllowOnHideFinalize = false
        popup:Hide()
    end)

    if popup then
        popup._paLiveVisibleSession = nil
        popup._paAllowOnHideFinalize = nil
    end

    if ok == false then
        return false, err or reason or "non-consuming-hide-failed"
    end
    return true, reason or "non-consuming-hide"
end

function PortalAuthority:_HideQualifyingUpgradeAnnouncementPopupForSetup()
    return self:_HideQualifyingUpgradeAnnouncementPopupNonConsuming("setup-hide")
end

function PortalAuthority:_FinalizeQualifyingUpgradeAnnouncementPopup(reason, opts)
    opts = type(opts) == "table" and opts or {}
    local popup = self._qualifyingUpgradeAnnouncementPopupFrame
    local openSettings = opts.openSettings == true
    local alreadyHidden = opts.alreadyHidden == true

    if popup and popup._paDismissFinalized ~= true then
        popup._paDismissFinalized = true
        popup._paAllowOnHideFinalize = false
        popup._paLiveVisibleSession = nil
        self:MarkQualifyingUpgradeWelcomeSatisfiedIfPending()
    end

    if not alreadyHidden and PA_IsPremiumWelcomeVisible(popup) then
        popup:Hide()
    end

    if openSettings then
        return self:OpenPrimarySettingsEntry("qualifying-upgrade-popup-open")
    end
    return true, reason or "dismissed"
end

function PortalAuthority:DismissQualifyingUpgradeAnnouncementPopup(reason)
    return self:_FinalizeQualifyingUpgradeAnnouncementPopup(reason or "dismissed", {
        openSettings = false,
        alreadyHidden = false,
    })
end

function PortalAuthority:OpenSettingsFromQualifyingUpgradeAnnouncement()
    return self:_FinalizeQualifyingUpgradeAnnouncementPopup("open-settings", {
        openSettings = true,
        alreadyHidden = false,
    })
end

function PortalAuthority:EnsureQualifyingUpgradeAnnouncementPopupFrame()
    local popup = self._qualifyingUpgradeAnnouncementPopupFrame
    if popup then
        return popup
    end

    popup = CreateFrame("Frame", "PortalAuthorityQualifyingUpgradePopupFrame", UIParent, "BackdropTemplate")
    self._qualifyingUpgradeAnnouncementPopupFrame = popup
    popup:SetSize(360, 342)
    popup:SetPoint("CENTER", UIParent, "CENTER", 0, 18)
    popup:SetFrameStrata("DIALOG")
    popup:SetFrameLevel(138)
    popup:SetToplevel(true)
    popup:SetClampedToScreen(true)
    popup:EnableMouse(true)
    popup:SetMovable(false)
    popup:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    popup:SetBackdropColor(0.039216, 0.039216, 0.062745, 1.0)
    popup:SetBackdropBorderColor(0.101961, 0.101961, 0.156863, 1.0)
    popup:Hide()
    if popup.GetName and popup:GetName() then
        PA_EnsureSpecialFrame(popup:GetName())
    end

    local stripe = CreateFrame("Frame", nil, popup)
    stripe:SetPoint("TOPLEFT", popup, "TOPLEFT", 0, 0)
    stripe:SetPoint("TOPRIGHT", popup, "TOPRIGHT", 0, 0)
    stripe:SetHeight(3)
    stripe:SetFrameLevel((popup:GetFrameLevel() or 138) + 6)
    popup._stripe = stripe

    local function setHorizontalGradient(texture, leftColor, rightColor)
        local leftR, leftG, leftB, leftA = unpack(leftColor)
        local rightR, rightG, rightB, rightA = unpack(rightColor)
        if texture.SetGradientAlpha then
            texture:SetGradientAlpha("HORIZONTAL", leftR, leftG, leftB, leftA, rightR, rightG, rightB, rightA)
        elseif texture.SetGradient and CreateColor then
            texture:SetGradient("HORIZONTAL", CreateColor(leftR, leftG, leftB, leftA), CreateColor(rightR, rightG, rightB, rightA))
        else
            texture:SetColorTexture(
                (leftR + rightR) * 0.5,
                (leftG + rightG) * 0.5,
                (leftB + rightB) * 0.5,
                (leftA + rightA) * 0.5
            )
        end
    end

    local stripeLeft = stripe:CreateTexture(nil, "ARTWORK")
    stripeLeft:SetPoint("TOPLEFT", stripe, "TOPLEFT", 0, 0)
    stripeLeft:SetPoint("BOTTOMLEFT", stripe, "BOTTOMLEFT", 0, 0)
    stripeLeft:SetWidth(180)
    stripeLeft:SetTexture("Interface\\Buttons\\WHITE8x8")
    setHorizontalGradient(stripeLeft, { 0.42, 0.37, 0.81, 1.0 }, { 0.61, 0.19, 1.0, 1.0 })

    local stripeRight = stripe:CreateTexture(nil, "ARTWORK")
    stripeRight:SetPoint("TOPLEFT", stripeLeft, "TOPRIGHT", 0, 0)
    stripeRight:SetPoint("BOTTOMLEFT", stripeLeft, "BOTTOMRIGHT", 0, 0)
    stripeRight:SetWidth(180)
    stripeRight:SetTexture("Interface\\Buttons\\WHITE8x8")
    setHorizontalGradient(stripeRight, { 0.61, 0.19, 1.0, 1.0 }, { 0.88, 0.25, 0.63, 1.0 })

    local content = CreateFrame("Frame", nil, popup)
    content:SetPoint("TOPLEFT", popup, "TOPLEFT", 26, -22)
    content:SetPoint("BOTTOMRIGHT", popup, "BOTTOMRIGHT", -26, 22)
    popup._content = content

    local logo = content:CreateTexture(nil, "ARTWORK")
    logo:SetSize(88, 88)
    logo:SetPoint("TOP", content, "TOP", 0, -16)
    logo:SetTexture("Interface\\AddOns\\PortalAuthority\\Media\\Images\\PA-Large-Transparent.png")
    popup._logo = logo

    local logoGlowOuter = content:CreateTexture(nil, "BACKGROUND")
    logoGlowOuter:SetSize(172, 172)
    logoGlowOuter:SetPoint("CENTER", logo, "CENTER", 0, -2)
    logoGlowOuter:SetTexture("Interface\\AddOns\\PortalAuthority\\Media\\Images\\PA_Welcome_Aura.png")
    logoGlowOuter:SetBlendMode("ADD")
    logoGlowOuter:SetVertexColor(0.53, 0.18, 0.86, 0.05)

    local logoGlowMid = content:CreateTexture(nil, "BACKGROUND")
    logoGlowMid:SetSize(142, 142)
    logoGlowMid:SetPoint("CENTER", logo, "CENTER", 0, -2)
    logoGlowMid:SetTexture("Interface\\AddOns\\PortalAuthority\\Media\\Images\\PA_Welcome_Aura.png")
    logoGlowMid:SetBlendMode("ADD")
    logoGlowMid:SetVertexColor(0.63, 0.23, 0.92, 0.07)

    local logoGlowInner = content:CreateTexture(nil, "BACKGROUND")
    logoGlowInner:SetSize(112, 112)
    logoGlowInner:SetPoint("CENTER", logo, "CENTER", 0, -2)
    logoGlowInner:SetTexture("Interface\\AddOns\\PortalAuthority\\Media\\Images\\PA_Welcome_Aura.png")
    logoGlowInner:SetBlendMode("ADD")
    logoGlowInner:SetVertexColor(0.74, 0.29, 0.98, 0.08)
    popup._logoGlow = logoGlowInner

    local title = content:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    PA_ApplyOnboardingPremiumFont(self, title, 20)
    title:SetPoint("TOP", logo, "BOTTOM", 0, -8)
    title:SetJustifyH("CENTER")
    title:SetWordWrap(false)
    title:SetTextColor(0.88, 0.88, 0.91, 1.0)
    title:SetText(L["RUNTIME_ONBOARDING_QUALIFYING_UPGRADE_TITLE"])
    popup._title = title

    local versionLine = content:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    PA_ApplyOnboardingLocalizedTextFont(self, versionLine, 11)
    versionLine:SetPoint("TOP", title, "BOTTOM", 0, -8)
    versionLine:SetJustifyH("CENTER")
    versionLine:SetWordWrap(false)
    versionLine:SetTextColor(0.36, 0.75, 0.71, 1.0)
    popup._versionLine = versionLine

    local divider = CreateFrame("Frame", nil, content)
    divider:SetSize(40, 1)
    divider:SetPoint("TOP", versionLine, "BOTTOM", 0, -16)
    popup._divider = divider

    local dividerLeft = divider:CreateTexture(nil, "ARTWORK")
    dividerLeft:SetPoint("TOPLEFT", divider, "TOPLEFT", 0, 0)
    dividerLeft:SetPoint("BOTTOMLEFT", divider, "BOTTOMLEFT", 0, 0)
    dividerLeft:SetWidth(13)
    dividerLeft:SetColorTexture(0.16, 0.16, 0.23, 0.25)

    local dividerMid = divider:CreateTexture(nil, "ARTWORK")
    dividerMid:SetPoint("TOPLEFT", dividerLeft, "TOPRIGHT", 0, 0)
    dividerMid:SetPoint("BOTTOMLEFT", dividerLeft, "BOTTOMRIGHT", 0, 0)
    dividerMid:SetWidth(14)
    dividerMid:SetColorTexture(0.16, 0.16, 0.23, 0.75)

    local dividerRight = divider:CreateTexture(nil, "ARTWORK")
    dividerRight:SetPoint("TOPLEFT", dividerMid, "TOPRIGHT", 0, 0)
    dividerRight:SetPoint("TOPRIGHT", divider, "TOPRIGHT", 0, 0)
    dividerRight:SetPoint("BOTTOMLEFT", dividerMid, "BOTTOMRIGHT", 0, 0)
    dividerRight:SetPoint("BOTTOMRIGHT", divider, "BOTTOMRIGHT", 0, 0)
    dividerRight:SetColorTexture(0.16, 0.16, 0.23, 0.25)

    local message = content:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    PA_ApplyOnboardingLocalizedTextFont(self, message, 13)
    message:SetPoint("TOPLEFT", divider, "BOTTOMLEFT", -98, -16)
    message:SetPoint("TOPRIGHT", divider, "BOTTOMRIGHT", 98, -16)
    message:SetJustifyH("CENTER")
    message:SetJustifyV("MIDDLE")
    if message.SetSpacing then
        message:SetSpacing(6)
    end
    message:SetTextColor(0.47, 0.47, 0.47, 1.0)
    message:SetText(L["RUNTIME_ONBOARDING_QUALIFYING_UPGRADE_MESSAGE"])
    popup._message = message

    local function createActionButton(parent, width, height, textValue)
        local PRESS_TEXT_OFFSET_X = 1
        local PRESS_TEXT_OFFSET_Y = -1

        local function setActionButtonTextOffset(btn, offsetX, offsetY)
            if not btn or not btn._text then
                return
            end
            btn._text:ClearAllPoints()
            btn._text:SetPoint("CENTER", btn, "CENTER", offsetX or 0, offsetY or 0)
        end

        local function clearActionButtonPressState(btn)
            if not btn then
                return
            end
            btn._paPressed = false
            setActionButtonTextOffset(btn, 0, 0)
        end

        local button = CreateFrame("Button", nil, parent, "BackdropTemplate")
        button:SetSize(width, height)
        button:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            edgeSize = 1,
        })
        button:SetBackdropColor(0.42, 0.37, 0.81, 0.12)
        button:SetBackdropBorderColor(0.42, 0.37, 0.81, 0.30)

        local text = button:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
        PA_ApplyOnboardingLocalizedTextFont(PortalAuthority, text, 13)
        text:SetPoint("CENTER")
        text:SetJustifyH("CENTER")
        text:SetWordWrap(false)
        text:SetTextColor(0.77, 0.75, 0.94, 1.0)
        if text.SetShadowOffset then
            text:SetShadowOffset(0, 0)
        end
        if text.SetShadowColor then
            text:SetShadowColor(0, 0, 0, 0)
        end
        text:SetText(textValue)
        button._text = text

        button:SetScript("OnEnter", function(selfButton)
            selfButton:SetBackdropBorderColor(0.42, 0.37, 0.81, 0.50)
        end)
        button:SetScript("OnLeave", function(selfButton)
            clearActionButtonPressState(selfButton)
            selfButton:SetBackdropColor(0.42, 0.37, 0.81, 0.12)
            selfButton:SetBackdropBorderColor(0.42, 0.37, 0.81, 0.30)
        end)
        button:SetScript("OnMouseDown", function(selfButton)
            selfButton._paPressed = true
            setActionButtonTextOffset(selfButton, PRESS_TEXT_OFFSET_X, PRESS_TEXT_OFFSET_Y)
        end)
        button:SetScript("OnMouseUp", function(selfButton)
            clearActionButtonPressState(selfButton)
        end)
        button:HookScript("OnHide", function(selfButton)
            clearActionButtonPressState(selfButton)
        end)
        return button
    end

    local openSettingsButton = createActionButton(content, 308, 34, L["COMMON_OPEN_SETTINGS"])
    openSettingsButton:SetPoint("BOTTOM", content, "BOTTOM", 0, 28)
    popup._openSettingsButton = openSettingsButton

    local laterButton = CreateFrame("Button", nil, content)
    laterButton:SetSize(64, 18)
    laterButton:SetPoint("TOP", openSettingsButton, "BOTTOM", 0, -10)
    laterButton:SetHitRectInsets(-6, -6, -4, -4)
    popup._laterButton = laterButton

    local laterText = laterButton:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    PA_ApplyOnboardingLocalizedTextFont(self, laterText, 11)
    laterText:SetPoint("CENTER")
    laterText:SetText(L["COMMON_LATER"])
    laterText:SetTextColor(0.34, 0.35, 0.41, 1.0)
    if laterText.SetShadowOffset then
        laterText:SetShadowOffset(0, 0)
    end
    if laterText.SetShadowColor then
        laterText:SetShadowColor(0, 0, 0, 0)
    end
    laterButton._text = laterText

    laterButton:SetScript("OnEnter", function(selfButton)
        selfButton._text:SetTextColor(0.43, 0.44, 0.50, 1.0)
    end)
    laterButton:SetScript("OnLeave", function(selfButton)
        selfButton._text:SetTextColor(0.34, 0.35, 0.41, 1.0)
    end)

    openSettingsButton:SetScript("OnClick", function()
        local ok, err = PortalAuthority:OpenSettingsFromQualifyingUpgradeAnnouncement()
        if not ok and err then
            PA_PrintPortalAuthorityMessage(err)
        end
    end)

    laterButton:SetScript("OnClick", function()
        PortalAuthority:DismissQualifyingUpgradeAnnouncementPopup("later")
    end)

    popup:SetScript("OnShow", function(frame)
        frame._paDismissFinalized = nil
        frame._paAllowOnHideFinalize = true
        frame._paLiveVisibleSession = true
        if PortalAuthority then
            PortalAuthority._qualifyingUpgradeAnnouncementTeardownSuppressed = nil
        end
        if frame.Raise then
            frame:Raise()
        end
    end)

    popup:SetScript("OnHide", function(frame)
        PA_StopFrameAnimation(frame)
        if frame
            and frame._paLiveVisibleSession == true
            and frame._paAllowOnHideFinalize == true
            and frame._paDismissFinalized ~= true
            and PortalAuthority
            and not PortalAuthority:_IsQualifyingUpgradeAnnouncementPopupTeardownSuppressed()
            and not PortalAuthority:_IsQualifyingUpgradeAnnouncementPopupNonConsumingHideSuppressed()
        then
            PortalAuthority:_FinalizeQualifyingUpgradeAnnouncementPopup("special-frame-close", {
                openSettings = false,
                alreadyHidden = true,
            })
        end
        frame._paDismissFinalized = nil
        frame._paAllowOnHideFinalize = nil
        frame._paLiveVisibleSession = nil
        frame:SetAlpha(1)
    end)

    return popup
end

function PortalAuthority:ShowQualifyingUpgradeAnnouncementPopup(reason)
    if not self:IsQualifyingUpgradeWelcomePending() then
        return false, L["RUNTIME_ONBOARDING_QUALIFYING_PENDING_NONE"]
    end

    local popup = self:EnsureQualifyingUpgradeAnnouncementPopupFrame()
    if not popup then
        return false, L["RUNTIME_ONBOARDING_QUALIFYING_POPUP_UNAVAILABLE"]
    end

    if popup._versionLine then
        popup._versionLine:SetText(string.format(L["RUNTIME_ONBOARDING_VERSION_FORMAT"], tostring(self:GetAnnouncementReleaseVersionString() or "")))
    end

    if PA_IsPremiumWelcomeVisible(popup) then
        if popup.Raise then
            popup:Raise()
        end
        return true, string.format(L["RUNTIME_ONBOARDING_QUALIFYING_POPUP_ALREADY_VISIBLE_FORMAT"], tostring(reason or "manual"))
    end

    popup._paDismissFinalized = nil
    popup._paAllowOnHideFinalize = true
    popup._paLiveVisibleSession = nil
    popup:SetAlpha(0)
    popup:Show()
    if popup.Raise then
        popup:Raise()
    end
    PA_StartAlphaTween(popup, popup, "fadeToken", 0, 1, 0.15)
    return true, string.format(L["RUNTIME_ONBOARDING_QUALIFYING_POPUP_SHOWN_FORMAT"], tostring(reason or "manual"))
end

function PortalAuthority:GetSettingsWindowAnnouncementTargetVersion()
    local targetVersion = trim(tostring(self:GetAnnouncementReleaseVersionString()
            or PortalAuthority._buildConfig.release.settingsWindowAnnouncementVersion
            or ""))
    if targetVersion == "" then
        return nil
    end
    return targetVersion
end

local function PA_ReadSettingsWindowAnnouncementState(authority)
    if not authority then
        return {
            lastAddonVersion = nil,
            pendingVersion = nil,
            seenVersion = nil,
        }
    end
    return {
        lastAddonVersion = authority.Profiles_GetSettingsWindowAnnouncementLastAddonVersion
                and authority:Profiles_GetSettingsWindowAnnouncementLastAddonVersion()
            or nil,
        pendingVersion = authority.Profiles_GetSettingsWindowAnnouncementPendingVersion
                and authority:Profiles_GetSettingsWindowAnnouncementPendingVersion()
            or nil,
        seenVersion = authority.Profiles_GetSettingsWindowAnnouncementSeenVersion
                and authority:Profiles_GetSettingsWindowAnnouncementSeenVersion()
            or nil,
    }
end

local function PA_IsSettingsWindowAnnouncementStateUninitialized(state)
    state = type(state) == "table" and state or {}
    return state.lastAddonVersion == nil and state.pendingVersion == nil and state.seenVersion == nil
end

function PortalAuthority:MigrateLegacySettingsWindowAnnouncementState(targetVersion, currentAddonVersion, currentVersionValid)
    targetVersion = trim(tostring(targetVersion or ""))
    if targetVersion == "" then
        return false, "Settings announcement target version is unavailable."
    end

    local rolloutVersion = trim(tostring(PortalAuthority._buildConfig.release.qualifyingUpgradeOnboardingVersion or ""))
    local legacyApplied = self.Profiles_GetQualifyingUpgradeOnboardingAppliedVersion
            and self:Profiles_GetQualifyingUpgradeOnboardingAppliedVersion()
        or nil
    local legacySeen = self.Profiles_GetQualifyingUpgradeOnboardingSeenVersion
            and self:Profiles_GetQualifyingUpgradeOnboardingSeenVersion()
        or nil

    if rolloutVersion ~= "" and legacySeen == rolloutVersion then
        if self.Profiles_SetSettingsWindowAnnouncementSeenVersion then
            self:Profiles_SetSettingsWindowAnnouncementSeenVersion(targetVersion)
        end
        if self.Profiles_SetSettingsWindowAnnouncementPendingVersion then
            self:Profiles_SetSettingsWindowAnnouncementPendingVersion(nil)
        end
        if currentVersionValid == true and self.Profiles_SetSettingsWindowAnnouncementLastAddonVersion then
            self:Profiles_SetSettingsWindowAnnouncementLastAddonVersion(currentAddonVersion)
        end
        return true, "Migrated legacy seen settings announcement state."
    end

    if rolloutVersion ~= ""
        and legacyApplied == rolloutVersion
        and legacySeen ~= rolloutVersion
    then
        if self.Profiles_SetSettingsWindowAnnouncementPendingVersion then
            self:Profiles_SetSettingsWindowAnnouncementPendingVersion(targetVersion)
        end
        if self.Profiles_SetSettingsWindowAnnouncementSeenVersion then
            self:Profiles_SetSettingsWindowAnnouncementSeenVersion(nil)
        end
        if currentVersionValid == true and self.Profiles_SetSettingsWindowAnnouncementLastAddonVersion then
            self:Profiles_SetSettingsWindowAnnouncementLastAddonVersion(currentAddonVersion)
        end
        return true, "Migrated legacy pending settings announcement state."
    end

    if currentVersionValid == true and self.Profiles_SetSettingsWindowAnnouncementLastAddonVersion then
        self:Profiles_SetSettingsWindowAnnouncementLastAddonVersion(currentAddonVersion)
        return true, "Initialized settings announcement version baseline."
    end

    return false, "Settings announcement migration is waiting for a valid addon version."
end

function PortalAuthority:ApplyPublicOnboardingD5b1FreshInstallState()
    if not PA_IsOnboardingD3PublicGateEnabled() then
        return false, "Public onboarding is disabled."
    end
    if self._profilesFreshInstallThisLoad ~= true or self._profilesLegacyMigratedThisLoad == true then
        return false, "No fresh-install onboarding state changes are needed."
    end

    if self.Profiles_SetSettingsResetEpochApplied then
        self:Profiles_SetSettingsResetEpochApplied(PortalAuthority._buildConfig.release.premiumOnboardingResetEpoch)
    end
    if self.Profiles_SetOnboardingExperienceSeen then
        self:Profiles_SetOnboardingExperienceSeen(nil)
    end
    if self.Profiles_SetFirstRunLandingConsumed then
        self:Profiles_SetFirstRunLandingConsumed(nil)
    end
    if self.Profiles_ClearOnboardingD4Session then
        self:Profiles_ClearOnboardingD4Session()
    end
    if self.Profiles_ClearQualifyingUpgradeOnboardingState then
        self:Profiles_ClearQualifyingUpgradeOnboardingState()
    end
    if self.Profiles_ClearSettingsWindowAnnouncementState then
        self:Profiles_ClearSettingsWindowAnnouncementState()
    end

    return true, "Fresh-install onboarding state prepared."
end

function PortalAuthority:ApplyPublicOnboardingD5b2QualifyingUpgradeState()
    if not PA_IsOnboardingQualifyingUpgradePublicGateEnabled() then
        return false, "Public qualifying-upgrade onboarding is disabled."
    end
    if not PA_IsOnboardingD3PublicGateEnabled() then
        return false, "Public onboarding is disabled."
    end

    if not self.Profiles_GetSettingsWindowAnnouncementLastAddonVersion
        or not self.Profiles_SetSettingsWindowAnnouncementLastAddonVersion
        or not self.Profiles_GetSettingsWindowAnnouncementPendingVersion
        or not self.Profiles_SetSettingsWindowAnnouncementPendingVersion
        or not self.Profiles_GetSettingsWindowAnnouncementSeenVersion
        or not self.Profiles_SetSettingsWindowAnnouncementSeenVersion
    then
        return false, "Qualifying-upgrade onboarding state storage is unavailable."
    end

    local targetVersion = self:GetSettingsWindowAnnouncementTargetVersion()
    if targetVersion == nil then
        return false, "Settings announcement target version is unavailable."
    end

    local currentAddonVersion, currentVersionValid = PA_GetCurrentAddonVersionState()
    local state = PA_ReadSettingsWindowAnnouncementState(self)
    local didChange = false
    local lastMessage = nil

    if PA_IsSettingsWindowAnnouncementStateUninitialized(state) then
        didChange, lastMessage = self:MigrateLegacySettingsWindowAnnouncementState(
            targetVersion,
            currentAddonVersion,
            currentVersionValid
        )
        state = PA_ReadSettingsWindowAnnouncementState(self)
    end

    if currentVersionValid ~= true then
        return didChange, lastMessage or "Settings announcement gating is waiting for a valid addon version."
    end

    if state.pendingVersion == targetVersion and state.seenVersion ~= targetVersion then
        self:Profiles_SetSettingsWindowAnnouncementLastAddonVersion(currentAddonVersion)
        return didChange, lastMessage or "Settings announcement is already pending."
    end

    if PA_IsSemverStringValid(state.lastAddonVersion)
        and PA_CompareSemver(state.lastAddonVersion, targetVersion) < 0
        and PA_CompareSemver(currentAddonVersion, targetVersion) >= 0
        and state.seenVersion ~= targetVersion
    then
        if state.pendingVersion ~= targetVersion then
            self:Profiles_SetSettingsWindowAnnouncementPendingVersion(targetVersion)
            didChange = true
            lastMessage = "Settings announcement state prepared."
        end
    end

    self:Profiles_SetSettingsWindowAnnouncementLastAddonVersion(currentAddonVersion)
    if didChange then
        return true, lastMessage or "Settings announcement state prepared."
    end
    return false, lastMessage or "No settings announcement state changes are needed."
end

function PortalAuthority:IsPremiumWelcomePending()
    local resetEpoch = self.Profiles_GetSettingsResetEpochApplied and self:Profiles_GetSettingsResetEpochApplied() or nil
    local experienceSeen = self.Profiles_GetOnboardingExperienceSeen and self:Profiles_GetOnboardingExperienceSeen() or nil
    return resetEpoch == PortalAuthority._buildConfig.release.premiumOnboardingResetEpoch
        and experienceSeen ~= PortalAuthority._buildConfig.release.premiumOnboardingVersion
end

function PortalAuthority:IsQualifyingUpgradeWelcomePending()
    if not PA_IsOnboardingQualifyingUpgradePublicGateEnabled() then
        return false
    end

    local targetVersion = self:GetSettingsWindowAnnouncementTargetVersion()
    if targetVersion == nil then
        return false
    end

    local pendingVersion = self.Profiles_GetSettingsWindowAnnouncementPendingVersion
            and self:Profiles_GetSettingsWindowAnnouncementPendingVersion()
        or nil
    local seenVersion = self.Profiles_GetSettingsWindowAnnouncementSeenVersion
            and self:Profiles_GetSettingsWindowAnnouncementSeenVersion()
        or nil
    return pendingVersion == targetVersion and seenVersion ~= targetVersion
end

function PortalAuthority:HasPendingPremiumWelcome()
    return self:IsPremiumWelcomePending() or self:IsQualifyingUpgradeWelcomePending()
end

function PortalAuthority:MarkPremiumOnboardingEntrySatisfied()
    if self.Profiles_SetOnboardingExperienceSeen then
        self:Profiles_SetOnboardingExperienceSeen(PortalAuthority._buildConfig.release.premiumOnboardingVersion)
    end
    if self.Profiles_SetFirstRunLandingConsumed then
        self:Profiles_SetFirstRunLandingConsumed(true)
    end
    return true
end

function PortalAuthority:MarkQualifyingUpgradeWelcomeSatisfiedIfPending()
    if not self:IsQualifyingUpgradeWelcomePending() then
        return false
    end

    local targetVersion = self:GetSettingsWindowAnnouncementTargetVersion()
    if targetVersion == nil then
        return false
    end
    if self.Profiles_SetSettingsWindowAnnouncementSeenVersion then
        self:Profiles_SetSettingsWindowAnnouncementSeenVersion(targetVersion)
    end
    if self.Profiles_SetSettingsWindowAnnouncementPendingVersion then
        self:Profiles_SetSettingsWindowAnnouncementPendingVersion(nil)
    end
    return true
end

function PortalAuthority:ScheduleAutomaticPremiumWelcome(reason)
    if not PA_IsOnboardingD3Enabled() then
        return false, L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_UNAVAILABLE_BUILD"]
    end
    if not self:IsPremiumWelcomePending() then
        return false, L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_PENDING_NONE"]
    end

    self._premiumWelcomeReadyAt = (GetTime and (GetTime() + 1.0)) or 0
    self:EnsurePremiumWelcomeFrame()

    if not (C_Timer and C_Timer.After) then
        return false, L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_TIMER_UNAVAILABLE"]
    end

    local token = math.floor((tonumber(self._premiumWelcomeAutoToken) or 0) + 1)
    self._premiumWelcomeAutoToken = token
    C_Timer.After(1.05, function()
        if not PortalAuthority or PortalAuthority._premiumWelcomeAutoToken ~= token then
            return
        end
        PortalAuthority:EvaluatePendingWelcome(reason or "login-auto")
    end)
    return true, L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_SCHEDULED"]
end

function PortalAuthority:CanShowQualifyingUpgradeAnnouncementNow()
    if not PA_IsOnboardingQualifyingUpgradePublicGateEnabled() then
        return false, L["RUNTIME_ONBOARDING_QUALIFYING_POPUP_UNAVAILABLE_BUILD"]
    end
    if (InCombatLockdown and InCombatLockdown()) or self:IsPlayerInCombat() then
        return false, L["RUNTIME_ONBOARDING_QUALIFYING_POPUP_UNAVAILABLE_COMBAT"]
    end
    if PA_IsChallengeActive and PA_IsChallengeActive() then
        return false, L["RUNTIME_ONBOARDING_QUALIFYING_POPUP_UNAVAILABLE_ACTIVE_MPLUS"]
    end

    local readyAt = tonumber(self._qualifyingUpgradeAnnouncementReadyAt or 0) or 0
    if readyAt > 0 and GetTime and GetTime() < readyAt then
        return false, L["RUNTIME_ONBOARDING_QUALIFYING_POPUP_WAITING_LOGIN"]
    end

    return true, L["COMMON_READY"]
end

function PortalAuthority:ScheduleAutomaticQualifyingUpgradeAnnouncement(reason)
    if not self:IsQualifyingUpgradeWelcomePending() then
        return false, L["RUNTIME_ONBOARDING_QUALIFYING_PENDING_NONE"]
    end

    self._qualifyingUpgradeAnnouncementReadyAt = (GetTime and (GetTime() + 1.0)) or 0
    self:EnsureQualifyingUpgradeAnnouncementPopupFrame()

    if not (C_Timer and C_Timer.After) then
        return false, L["RUNTIME_ONBOARDING_QUALIFYING_POPUP_TIMER_UNAVAILABLE"]
    end

    local token = math.floor((tonumber(self._qualifyingUpgradeAnnouncementAutoToken) or 0) + 1)
    self._qualifyingUpgradeAnnouncementAutoToken = token
    C_Timer.After(1.05, function()
        if not PortalAuthority or PortalAuthority._qualifyingUpgradeAnnouncementAutoToken ~= token then
            return
        end
        if not PortalAuthority:IsQualifyingUpgradeWelcomePending() then
            return
        end

        local canShow = PortalAuthority:CanShowQualifyingUpgradeAnnouncementNow()
        if not canShow then
            return
        end

        PortalAuthority:ShowQualifyingUpgradeAnnouncementPopup(reason or "login-auto")
    end)
    return true, L["RUNTIME_ONBOARDING_QUALIFYING_POPUP_SCHEDULED"]
end

function PortalAuthority:CanShowPremiumWelcomeNow()
    if not PA_IsOnboardingD3Enabled() then
        return false, L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_UNAVAILABLE_BUILD"]
    end
    if (InCombatLockdown and InCombatLockdown()) or self:IsPlayerInCombat() then
        return false, L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_UNAVAILABLE_COMBAT"]
    end
    if PA_IsChallengeActive and PA_IsChallengeActive() then
        return false, L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_UNAVAILABLE_ACTIVE_MPLUS"]
    end

    local readyAt = tonumber(self._premiumWelcomeReadyAt or 0) or 0
    if readyAt > 0 and GetTime and GetTime() < readyAt then
        return false, L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_WAITING_LOGIN"]
    end

    return true, L["COMMON_READY"]
end

function PortalAuthority:HidePremiumWelcome()
    if PA_IsPremiumWelcomeVisible(self._premiumWelcomeFrame) then
        self._premiumWelcomeFrame:Hide()
    end
end

function PortalAuthority:EnsurePremiumWelcomeFrame()
    local overlay = self._premiumWelcomeFrame
    if overlay then
        return overlay
    end

    overlay = CreateFrame("Frame", "PortalAuthorityPremiumWelcomeFrame", UIParent, "BackdropTemplate")
    self._premiumWelcomeFrame = overlay
    overlay:SetSize(544, 448)
    overlay:SetPoint("CENTER", UIParent, "CENTER", 0, 18)
    overlay:SetFrameStrata("DIALOG")
    overlay:SetFrameLevel(120)
    overlay:SetToplevel(true)
    overlay:SetClampedToScreen(true)
    overlay:EnableMouse(true)
    overlay:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
    })
    overlay:SetBackdropColor(0.04, 0.05, 0.07, 0.97)
    overlay:Hide()
    overlay:SetScript("OnMouseDown", function() end)
    overlay:SetScript("OnMouseUp", function() end)

    local content = CreateFrame("Frame", nil, overlay, "BackdropTemplate")
    content:SetSize(480, 392)
    content:SetPoint("CENTER", overlay, "CENTER", 0, 10)
    content:SetFrameLevel((overlay:GetFrameLevel() or 120) + 3)
    content:EnableMouse(false)
    overlay._content = content

    local logo = content:CreateTexture(nil, "ARTWORK")
    logo:SetTexture("Interface\\AddOns\\PortalAuthority\\Media\\Images\\PA-Large-Transparent.png")
    logo:SetPoint("TOP", content, "TOP", 0, -30)
    logo:SetSize(140, 140)
    overlay._logo = logo

    overlay._aura = nil

    local wordmark = content:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    PA_ApplyOnboardingPremiumFont(self, wordmark, 32)
    wordmark:SetPoint("TOP", logo, "BOTTOM", 0, -22)
    wordmark:SetJustifyH("CENTER")
    wordmark:SetWordWrap(false)
    wordmark:SetTextColor(0.97, 0.97, 0.98, 1.0)
    wordmark:SetShadowOffset(0, -1)
    wordmark:SetShadowColor(0, 0, 0, 0.55)
    wordmark:SetText(L["RUNTIME_ONBOARDING_GUIDED_SETUP_WORDMARK"])
    overlay._wordmark = wordmark

    local valueLine = content:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
    PA_ApplyOnboardingLocalizedTextFont(self, valueLine, 16)
    valueLine:SetPoint("TOP", wordmark, "BOTTOM", 0, -10)
    valueLine:SetJustifyH("CENTER")
    valueLine:SetWordWrap(false)
    valueLine:SetTextColor(0.89, 0.88, 0.90, 1.0)
    valueLine:SetText(L["RUNTIME_ONBOARDING_VALUE_LINE"])
    overlay._valueLine = valueLine

    local helperLine = content:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    PA_ApplyOnboardingLocalizedTextFont(self, helperLine, 13)
    helperLine:SetPoint("TOP", valueLine, "BOTTOM", 0, -8)
    helperLine:SetJustifyH("CENTER")
    helperLine:SetWordWrap(false)
    helperLine:SetTextColor(0.68, 0.68, 0.72, 1.0)
    helperLine:SetText(L["RUNTIME_ONBOARDING_WELCOME_HELPER_LINE"])
    overlay._helperLine = helperLine

    local function createPrimaryButton(parent, label)
        local button = CreateFrame("Button", nil, parent, "BackdropTemplate")
        button:SetSize(220, 36)
        button:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            edgeSize = 1,
        })
        button:SetBackdropColor(0.16, 0.11, 0.23, 0.96)
        button:SetBackdropBorderColor(0.40, 0.28, 0.51, 0.74)

        local text = button:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
        PA_ApplyOnboardingLocalizedTextFont(PortalAuthority, text, 14)
        text:SetPoint("CENTER")
        text:SetTextColor(0.98, 0.97, 0.98, 1.0)
        text:SetText(label)
        button._text = text

        button:SetScript("OnEnter", function(selfBtn)
            selfBtn:SetBackdropColor(0.20, 0.13, 0.28, 0.98)
            selfBtn:SetBackdropBorderColor(0.50, 0.35, 0.64, 0.86)
        end)
        button:SetScript("OnLeave", function(selfBtn)
            selfBtn:SetBackdropColor(0.16, 0.11, 0.23, 0.96)
            selfBtn:SetBackdropBorderColor(0.40, 0.28, 0.51, 0.74)
        end)
        return button
    end

    local function createTextButton(parent, label, quietest)
        local button = CreateFrame("Button", nil, parent)
        local text = button:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        PA_ApplyOnboardingLocalizedTextFont(PortalAuthority, text, 12)
        text:SetPoint("CENTER")
        text:SetText(label)
        text:SetTextColor(quietest and 0.58 or 0.72, quietest and 0.58 or 0.72, quietest and 0.62 or 0.76, 1.0)
        text:SetShadowOffset(0, -1)
        text:SetShadowColor(0, 0, 0, 0.35)
        button._text = text
        button:SetSize(math.max(64, math.ceil((text:GetStringWidth() or 0) + 12)), 18)
        button:SetHitRectInsets(-6, -6, -4, -4)
        button:SetScript("OnEnter", function(selfBtn)
            if quietest then
                selfBtn._text:SetTextColor(0.76, 0.76, 0.80, 1.0)
            else
                selfBtn._text:SetTextColor(0.87, 0.87, 0.90, 1.0)
            end
        end)
        button:SetScript("OnLeave", function(selfBtn)
            selfBtn._text:SetTextColor(quietest and 0.58 or 0.72, quietest and 0.58 or 0.72, quietest and 0.62 or 0.76, 1.0)
        end)
        return button
    end

    local primaryButton = createPrimaryButton(content, L["COMMON_SHOW_ME_AROUND"])
    primaryButton:SetPoint("TOP", helperLine, "BOTTOM", 0, -34)
    overlay._primaryButton = primaryButton

    local secondaryRow = CreateFrame("Frame", nil, content)
    secondaryRow:SetSize(260, 20)
    secondaryRow:SetPoint("TOP", primaryButton, "BOTTOM", 0, -14)
    overlay._secondaryRow = secondaryRow

    local openSettingsButton = createTextButton(secondaryRow, L["COMMON_OPEN_SETTINGS"], false)
    openSettingsButton:SetPoint("CENTER", secondaryRow, "CENTER", -56, 0)
    overlay._openSettingsButton = openSettingsButton

    local skipButton = createTextButton(secondaryRow, L["COMMON_SKIP"], true)
    skipButton:SetPoint("CENTER", secondaryRow, "CENTER", 56, 0)
    overlay._skipButton = skipButton

    primaryButton:SetScript("OnClick", function()
        if not PortalAuthority then
            return
        end
        local ok, response = PortalAuthority:StartPremiumOnboardingExperience("welcome-primary")
        if not ok and not PA_IsOnboardingD4Enabled() then
            local openErr
            ok, openErr = PortalAuthority:OpenSettings(PortalAuthority.rootCategoryID)
            if ok then
                response = L["RUNTIME_ONBOARDING_PREMIUM_ROOT_HOME_OPENED_FALLBACK"]
            else
                response = openErr or response
            end
        end
        if ok then
            PortalAuthority:HidePremiumWelcome()
        else
            PA_PrintPortalAuthorityMessage(response or L["RUNTIME_ONBOARDING_PREMIUM_ONBOARDING_FAILED"])
        end
    end)

    openSettingsButton:SetScript("OnClick", function()
        if not PortalAuthority then
            return
        end
        local ok, err = PortalAuthority:OpenPrimarySettingsEntry("welcome-open-settings")
        if ok then
            PortalAuthority:HidePremiumWelcome()
        elseif err then
            PA_PrintPortalAuthorityMessage(err)
        end
    end)

    skipButton:SetScript("OnClick", function()
        if PortalAuthority then
            PortalAuthority:HidePremiumWelcome()
        end
    end)

    overlay:SetScript("OnShow", function(frame)
        frame:SetAlpha(1)
        if frame.Raise then
            frame:Raise()
        end
    end)

    overlay:SetScript("OnHide", function(frame)
        frame:SetAlpha(1)
    end)

    return overlay
end

function PortalAuthority:TryShowPremiumWelcome(reason, explicitEntry)
    if not PA_IsOnboardingD3Enabled() then
        return false, L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_UNAVAILABLE_BUILD"]
    end
    if not explicitEntry and not self:IsPremiumWelcomePending() then
        return false, L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_PENDING_NONE"]
    end

    local canShow, blockMessage = self:CanShowPremiumWelcomeNow()
    if not canShow then
        return false, blockMessage
    end

    local frame = self:EnsurePremiumWelcomeFrame()
    if not frame then
        return false, L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_FRAME_UNAVAILABLE"]
    end

    if PA_IsPremiumWelcomeVisible(frame) then
        if frame.Raise then
            frame:Raise()
        end
        return true, L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_ALREADY_VISIBLE"]
    end

    frame:Show()
    self:MarkPremiumOnboardingEntrySatisfied()

    return true, string.format(L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_SHOWN_FORMAT"], tostring(reason or "manual"))
end

function PortalAuthority:EvaluatePendingWelcome(reason)
    if not self:IsPremiumWelcomePending() then
        return false, L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_PENDING_NONE"]
    end
    return self:TryShowPremiumWelcome(reason, false)
end

function PortalAuthority:StartExplicitQualifyingUpgradeWelcome(reason)
    if not self:IsQualifyingUpgradeWelcomePending() then
        return false, L["RUNTIME_ONBOARDING_QUALIFYING_PENDING_NONE"]
    end
    return self:ShowQualifyingUpgradeAnnouncementPopup(reason or "qualifying-upgrade-explicit")
end

function PortalAuthority:ShowPremiumRootHome(reason, consumeLanding)
    if not PA_IsOnboardingD3Enabled() then
        return false, L["RUNTIME_ONBOARDING_PREMIUM_ROOT_HOME_UNAVAILABLE_BUILD"]
    end

    local rootAlreadyVisible = false
    if self.IsSettingsWindowHostEnabled
        and self:IsSettingsWindowHostEnabled()
        and self.IsSettingsWindowOpen
        and self:IsSettingsWindowOpen()
    then
        local runtime = self._settingsWindowRuntime
        rootAlreadyVisible = runtime and runtime.currentSectionKey == "root"
    end

    if not rootAlreadyVisible then
        local rootPanel = self.rootPanel
        rootAlreadyVisible = rootPanel and rootPanel.IsShown and rootPanel:IsShown() and true or false
    end

    if rootAlreadyVisible then
        if consumeLanding and self.Profiles_SetFirstRunLandingConsumed and not self:Profiles_GetFirstRunLandingConsumed() then
            self:Profiles_SetFirstRunLandingConsumed(true)
        end
        return true, string.format(L["RUNTIME_ONBOARDING_PREMIUM_ROOT_HOME_ALREADY_VISIBLE_FORMAT"], tostring(reason or "manual"))
    end

    local opened, openErr = self:OpenSettings(self.rootCategoryID)
    if not opened then
        return false, openErr or L["RUNTIME_ONBOARDING_PREMIUM_ROOT_HOME_UNAVAILABLE"]
    end

    if consumeLanding and self.Profiles_SetFirstRunLandingConsumed then
        self:Profiles_SetFirstRunLandingConsumed(true)
    end
    return true, string.format(L["RUNTIME_ONBOARDING_PREMIUM_ROOT_HOME_OPENED_FORMAT"], tostring(reason or "manual"))
end

function PortalAuthority:StartPremiumOnboardingExperience(reason)
    if not PA_IsOnboardingD3Enabled() then
        return false, L["RUNTIME_ONBOARDING_PREMIUM_ONBOARDING_UNAVAILABLE_BUILD"]
    end
    if PA_IsOnboardingD4Enabled() then
        return self:StartOnboardingD4Tour(reason or "premium-onboarding")
    end
    return self:ShowPremiumRootHome(reason or "premium-onboarding", false)
end

function PortalAuthority:StartExplicitPremiumOnboarding(reason)
    if not PA_IsOnboardingD3Enabled() then
        return false, L["RUNTIME_ONBOARDING_PREMIUM_ONBOARDING_UNAVAILABLE_BUILD"]
    end
    if not PA_IsOnboardingD4Enabled() then
        return false, L["RUNTIME_ONBOARDING_GUIDED_SETUP_UNAVAILABLE_BUILD"]
    end

    local shouldStampOnSuccess = not (self.IsOnboardingD4Active and self:IsOnboardingD4Active())
    local ok, response = self:StartOnboardingD4Tour(reason or "premium-onboarding")
    if ok and shouldStampOnSuccess then
        self:MarkPremiumOnboardingEntrySatisfied()
    end
    return ok, response
end

function PortalAuthority:OpenPrimarySettingsEntry(reason)
    if PA_IsOnboardingD3Enabled() then
        local landingConsumed = self.Profiles_GetFirstRunLandingConsumed and self:Profiles_GetFirstRunLandingConsumed() or nil
        if not landingConsumed then
            return self:ShowPremiumRootHome(reason or "primary-settings-entry", true)
        end
    end

    local opened, openErr = self:OpenSettings(nil)
    if not opened then
        return false, openErr or L["SETTINGS_WINDOW_UNAVAILABLE"]
    end
    return true, nil
end

function PortalAuthority:HandleSlashRootEntry()
    if self:IsQualifyingUpgradeWelcomePending() then
        return self:StartExplicitQualifyingUpgradeWelcome("slash-pa")
    end
    return self:OpenPrimarySettingsEntry("slash-pa")
end

function PortalAuthority:HandleSlashUpdatesEntry()
    if self:IsQualifyingUpgradeWelcomePending() then
        return self:StartExplicitQualifyingUpgradeWelcome("slash-updates")
    end
    return self:OpenPrimarySettingsEntry("slash-updates")
end

function PortalAuthority:GetOnboardingD3StatusLines()
    local lines = {}
    local resetEpoch = self.Profiles_GetSettingsResetEpochApplied and self:Profiles_GetSettingsResetEpochApplied() or nil
    local seen = self.Profiles_GetOnboardingExperienceSeen and self:Profiles_GetOnboardingExperienceSeen() or nil
    local landingConsumed = self.Profiles_GetFirstRunLandingConsumed and self:Profiles_GetFirstRunLandingConsumed() or nil
    local upgradeApplied = self.Profiles_GetQualifyingUpgradeOnboardingAppliedVersion and self:Profiles_GetQualifyingUpgradeOnboardingAppliedVersion() or nil
    local upgradeSeen = self.Profiles_GetQualifyingUpgradeOnboardingSeenVersion and self:Profiles_GetQualifyingUpgradeOnboardingSeenVersion() or nil
    local popupLastAddonVersion = self.Profiles_GetSettingsWindowAnnouncementLastAddonVersion
            and self:Profiles_GetSettingsWindowAnnouncementLastAddonVersion()
        or nil
    local popupPendingVersion = self.Profiles_GetSettingsWindowAnnouncementPendingVersion
            and self:Profiles_GetSettingsWindowAnnouncementPendingVersion()
        or nil
    local popupSeenVersion = self.Profiles_GetSettingsWindowAnnouncementSeenVersion
            and self:Profiles_GetSettingsWindowAnnouncementSeenVersion()
        or nil
    local currentAddonVersion, currentAddonVersionValid = PA_GetCurrentAddonVersionState()
    local pending = self:IsPremiumWelcomePending()
    local upgradePending = self:IsQualifyingUpgradeWelcomePending()
    local canShow, reason = self:CanShowPremiumWelcomeNow()

    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r D3 gate=%s", PA_IsOnboardingD3DevGateEnabled() and "ON" or "OFF")
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r D3 public gate=%s", PA_IsOnboardingD3PublicGateEnabled() and "ON" or "OFF")
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r qualifyingUpgradePublicGate=%s", PA_IsOnboardingQualifyingUpgradePublicGateEnabled() and "ON" or "OFF")
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r settingsResetEpochApplied=%s", tostring(resetEpoch))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r onboardingExperienceSeen=%s", tostring(seen))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r firstRunLandingConsumed=%s", tostring(landingConsumed))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r settingsAnnouncementCurrentAddonVersion=%s", tostring(currentAddonVersion))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r settingsAnnouncementCurrentAddonVersionValid=%s", tostring(currentAddonVersionValid))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r settingsAnnouncementLastAddonVersion=%s", tostring(popupLastAddonVersion))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r settingsAnnouncementPendingVersion=%s", tostring(popupPendingVersion))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r settingsAnnouncementSeenVersion=%s", tostring(popupSeenVersion))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r legacyQualifyingUpgradeAppliedVersion=%s", tostring(upgradeApplied))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r legacyQualifyingUpgradeSeenVersion=%s", tostring(upgradeSeen))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r pendingWelcome=%s", tostring(pending))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r qualifyingUpgradePending=%s", tostring(upgradePending))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r anyPendingWelcome=%s", tostring(self:HasPendingPremiumWelcome()))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r safeShow=%s (%s)", tostring(canShow), tostring(reason or "unknown"))
    return lines
end

function PortalAuthority:GetS8AnnouncementStatusLines()
    local lines = {}
    local releaseVersion = self:GetAnnouncementReleaseVersionString() or PortalAuthority._buildConfig.release.settingsWindowAnnouncementVersion
    local currentAddonVersion, currentAddonVersionValid = PA_GetCurrentAddonVersionState()
    local popupLastAddonVersion = self.Profiles_GetSettingsWindowAnnouncementLastAddonVersion
            and self:Profiles_GetSettingsWindowAnnouncementLastAddonVersion()
        or nil
    local popupPendingVersion = self.Profiles_GetSettingsWindowAnnouncementPendingVersion
            and self:Profiles_GetSettingsWindowAnnouncementPendingVersion()
        or nil
    local popupSeenVersion = self.Profiles_GetSettingsWindowAnnouncementSeenVersion
            and self:Profiles_GetSettingsWindowAnnouncementSeenVersion()
        or nil
    local legacyPopupVersion = PortalAuthority._buildConfig.release.qualifyingUpgradeOnboardingVersion
    local legacyPopupApplied = self.Profiles_GetQualifyingUpgradeOnboardingAppliedVersion
            and self:Profiles_GetQualifyingUpgradeOnboardingAppliedVersion()
        or nil
    local legacyPopupSeen = self.Profiles_GetQualifyingUpgradeOnboardingSeenVersion
            and self:Profiles_GetQualifyingUpgradeOnboardingSeenVersion()
        or nil
    local glowSeen = self.Profiles_GetSettingsWindowSearchIntroSeenVersion
            and self:Profiles_GetSettingsWindowSearchIntroSeenVersion()
        or nil
    local d3Pending = self:IsPremiumWelcomePending()
    local popupPending = self:IsQualifyingUpgradeWelcomePending()
    local landingConsumed = self.Profiles_GetFirstRunLandingConsumed
            and self:Profiles_GetFirstRunLandingConsumed()
        or nil
    local glowPending = glowSeen ~= releaseVersion
    local popupVisible = self._qualifyingUpgradeAnnouncementPopupFrame
            and self._qualifyingUpgradeAnnouncementPopupFrame.IsShown
            and self._qualifyingUpgradeAnnouncementPopupFrame:IsShown()
        or false
    local windowOpen = self.IsSettingsWindowOpen and self:IsSettingsWindowOpen() or false
    local routingState = "consumed-or-none"
    local routingNote = nil
    if popupPending and d3Pending then
        routingState = "qualifying-upgrade-pending"
        routingNote = "popup takes precedence over d3"
    elseif popupPending then
        routingState = "qualifying-upgrade-pending"
    elseif d3Pending and landingConsumed ~= true then
        routingState = "fresh-install-d3-pending"
    elseif d3Pending then
        routingState = "premium-pending-other"
    end

    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 releaseVersion=%s", tostring(releaseVersion))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 popupTargetVersion=%s", tostring(releaseVersion))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 popupCurrentAddonVersion=%s", tostring(currentAddonVersion))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 popupCurrentAddonVersionValid=%s", tostring(currentAddonVersionValid))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 popupLastAddonVersion=%s", tostring(popupLastAddonVersion))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 popupPendingVersion=%s", tostring(popupPendingVersion))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 popupSeenVersion=%s", tostring(popupSeenVersion))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 legacyPopupRolloutVersion=%s", tostring(legacyPopupVersion))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 legacyPopupApplied=%s", tostring(legacyPopupApplied))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 legacyPopupSeen=%s", tostring(legacyPopupSeen))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 popupPending=%s", tostring(popupPending))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 popupVisible=%s", tostring(popupVisible))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 d3Pending=%s", tostring(d3Pending))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 landingConsumed=%s", tostring(landingConsumed))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 routingState=%s", tostring(routingState))
    if routingNote then
        lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 routingNote=%s", tostring(routingNote))
    end
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 glowSeen=%s", tostring(glowSeen))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 glowPending=%s", tostring(glowPending))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r S8 windowOpen=%s", tostring(windowOpen))
    return lines
end

function PortalAuthority:_SuppressPremiumWelcomeStateForS8TestSetup()
    if self.Profiles_SetOnboardingExperienceSeen then
        self:Profiles_SetOnboardingExperienceSeen(PortalAuthority._buildConfig.release.premiumOnboardingVersion)
    end
    if self.Profiles_SetFirstRunLandingConsumed then
        self:Profiles_SetFirstRunLandingConsumed(true)
    end
    if self.Profiles_ClearOnboardingD4Session then
        self:Profiles_ClearOnboardingD4Session()
    end
    if self.HidePremiumWelcome then
        self:HidePremiumWelcome()
    end

    self._premiumWelcomeReadyAt = 0
    self._premiumWelcomeAutoToken = math.floor((tonumber(self._premiumWelcomeAutoToken) or 0) + 1)
    return true
end

function PortalAuthority:PrepareS8AnnouncementTestState(mode)
    mode = trim(tostring(mode or "")):lower()
    local releaseVersion = self:GetAnnouncementReleaseVersionString() or PortalAuthority._buildConfig.release.settingsWindowAnnouncementVersion
    local popupVersion = PortalAuthority._buildConfig.release.qualifyingUpgradeOnboardingVersion
    local currentAddonVersion, currentAddonVersionValid = PA_GetCurrentAddonVersionState()

    if mode == "upgrade" or mode == "test-upgrade" or mode == "all" or mode == "test-all" or mode == "reset" then
        if self.Profiles_SetQualifyingUpgradeOnboardingAppliedVersion then
            self:Profiles_SetQualifyingUpgradeOnboardingAppliedVersion(popupVersion)
        end
        if self.Profiles_SetQualifyingUpgradeOnboardingSeenVersion then
            self:Profiles_SetQualifyingUpgradeOnboardingSeenVersion(nil)
        end
        if self.Profiles_SetSettingsWindowAnnouncementPendingVersion then
            self:Profiles_SetSettingsWindowAnnouncementPendingVersion(releaseVersion)
        end
        if self.Profiles_SetSettingsWindowAnnouncementSeenVersion then
            self:Profiles_SetSettingsWindowAnnouncementSeenVersion(nil)
        end
        if currentAddonVersionValid == true and self.Profiles_SetSettingsWindowAnnouncementLastAddonVersion then
            self:Profiles_SetSettingsWindowAnnouncementLastAddonVersion(currentAddonVersion)
        end
    end

    if mode == "upgrade" or mode == "test-upgrade" or mode == "all" or mode == "test-all" or mode == "reset" then
        if self._SuppressPremiumWelcomeStateForS8TestSetup then
            self:_SuppressPremiumWelcomeStateForS8TestSetup()
        end
    end

    if mode == "glow" or mode == "test-glow" or mode == "all" or mode == "test-all" or mode == "reset" then
        if self.Profiles_ClearSettingsWindowSearchIntroSeenVersion then
            self:Profiles_ClearSettingsWindowSearchIntroSeenVersion()
        elseif self.Profiles_SetSettingsWindowSearchIntroSeenVersion then
            self:Profiles_SetSettingsWindowSearchIntroSeenVersion(nil)
        end
    end

    if mode == "fresh-install" or mode == "test-fresh-install" then
        if self.Profiles_ClearQualifyingUpgradeOnboardingState then
            self:Profiles_ClearQualifyingUpgradeOnboardingState()
        else
            if self.Profiles_SetQualifyingUpgradeOnboardingAppliedVersion then
                self:Profiles_SetQualifyingUpgradeOnboardingAppliedVersion(nil)
            end
            if self.Profiles_SetQualifyingUpgradeOnboardingSeenVersion then
                self:Profiles_SetQualifyingUpgradeOnboardingSeenVersion(nil)
            end
        end
        if self.Profiles_ClearSettingsWindowAnnouncementState then
            self:Profiles_ClearSettingsWindowAnnouncementState()
        end
        if self.Profiles_SetSettingsResetEpochApplied then
            self:Profiles_SetSettingsResetEpochApplied(PortalAuthority._buildConfig.release.premiumOnboardingResetEpoch)
        end
        if self.Profiles_SetOnboardingExperienceSeen then
            self:Profiles_SetOnboardingExperienceSeen(nil)
        end
        if self.Profiles_SetFirstRunLandingConsumed then
            self:Profiles_SetFirstRunLandingConsumed(nil)
        end
        if self.Profiles_ClearOnboardingD4Session then
            self:Profiles_ClearOnboardingD4Session()
        end
    end

    if self._HideQualifyingUpgradeAnnouncementPopupForSetup then
        self:_HideQualifyingUpgradeAnnouncementPopupForSetup()
    elseif self.HideQualifyingUpgradeAnnouncementPopup then
        self:HideQualifyingUpgradeAnnouncementPopup()
    end
    if self.CloseSettingsWindow and self.IsSettingsWindowOpen and self:IsSettingsWindowOpen() then
        self:CloseSettingsWindow()
    end

    if mode == "upgrade" or mode == "test-upgrade" then
        return true, string.format(
            "S8 popup re-armed and D3 welcome suppressed. Next: type /pa or /pa updates. Glow seen stays %s.",
            tostring(self.Profiles_GetSettingsWindowSearchIntroSeenVersion and self:Profiles_GetSettingsWindowSearchIntroSeenVersion() or nil)
        )
    end
    if mode == "glow" or mode == "test-glow" then
        return true, string.format(
            "S8 search glow re-armed for version %s. Next: open the custom settings window from hidden.",
            tostring(releaseVersion)
        )
    end
    if mode == "fresh-install" or mode == "test-fresh-install" then
        return true, string.format(
            "S8 fresh-install routing re-armed. Next: type /pa or /pa updates; the %s popup should not appear.",
            tostring(releaseVersion)
        )
    end
    if mode == "all" or mode == "test-all" or mode == "reset" then
        return true, "S8 popup + glow re-armed with D3 welcome suppressed. Next: type /pa, then use Open Settings in the popup."
    end

    return false, "Usage: /pa dev s8 status|test-upgrade|test-glow|test-fresh-install|test-all"
end

function PortalAuthority:HandleS8AnnouncementSlash(rawAction)
    local action = trim(tostring(rawAction or "")):lower()
    if action == "" or action == "help" then
        PA_PrintPortalAuthorityMessage("usage: /pa dev s8 status|test-upgrade|test-glow|test-fresh-install|test-all")
        return true
    end

    if action == "status" then
        for _, line in ipairs(self:GetS8AnnouncementStatusLines()) do
            print(line)
        end
        return true
    end

    local ok, message = self:PrepareS8AnnouncementTestState(action)
    PA_PrintPortalAuthorityMessage(message or (ok and "S8 test state prepared." or "S8 test state failed."))
    return true
end

function PortalAuthority:HandleOnboardingD3Slash(rawAction)
    local action = trim(tostring(rawAction or "")):lower()
    if action == "" or action == "help" then
        PA_PrintPortalAuthorityMessage("usage: /pa dev onboarding-d3 status|clear-intro-state|show-welcome|show-root")
        return true
    end

    if action == "status" then
        for _, line in ipairs(self:GetOnboardingD3StatusLines()) do
            print(line)
        end
        return true
    end

    if action == "clear-intro-state" then
        if not self.Profiles_ClearIntroState then
            PA_PrintPortalAuthorityMessage("D3 intro-state helpers are unavailable.")
            return true
        end
        self:Profiles_ClearIntroState()
        PA_PrintPortalAuthorityMessage("D3 intro-state cleared for the active profile.")
        return true
    end

    if action == "show-welcome" then
        local ok, response = self:TryShowPremiumWelcome("dev-show-welcome", true)
        PA_PrintPortalAuthorityMessage(response or (ok and L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_SHOWN"] or L["RUNTIME_ONBOARDING_PREMIUM_WELCOME_FAILED"]))
        return true
    end

    if action == "show-root" then
        local ok, response = self:ShowPremiumRootHome("dev-show-root", false)
        PA_PrintPortalAuthorityMessage(response or (ok and L["RUNTIME_ONBOARDING_PREMIUM_ROOT_HOME_OPENED"] or L["RUNTIME_ONBOARDING_PREMIUM_ROOT_HOME_FAILED"]))
        return true
    end

    PA_PrintPortalAuthorityMessage("usage: /pa dev onboarding-d3 status|clear-intro-state|show-welcome|show-root")
    return true
end

local PA_ONBOARDING_D4_CARD_WIDTH = 344
local PA_ONBOARDING_D4_CARD_FINAL_WIDTH = PA_ONBOARDING_D4_CARD_WIDTH
local PA_ONBOARDING_D4_CARD_HEIGHT = 228
local PA_ONBOARDING_D4_CARD_OFFSET_X = -260
local PA_ONBOARDING_D4_CARD_OFFSET_Y = 60
local PA_ONBOARDING_D4_MODULE_OFFSET_X = 260
local PA_ONBOARDING_D4_MODULE_OFFSET_Y = 60
local PA_ONBOARDING_D4_SETTINGS_NOTE = L["RUNTIME_ONBOARDING_SETTINGS_NOTE"]

local PA_ONBOARDING_D4_STEPS = {
    {
        key = "timers",
        title = L["RUNTIME_ONBOARDING_STEP_TIMERS_TITLE"],
        helper = L["RUNTIME_ONBOARDING_STEP_TIMERS_HELPER"],
        subhelper = L["RUNTIME_ONBOARDING_STEP_TIMERS_SUBHELPER"],
    },
    {
        key = "interruptTracker",
        title = L["RUNTIME_ONBOARDING_STEP_INTERRUPT_TITLE"],
        helper = L["RUNTIME_ONBOARDING_STEP_INTERRUPT_HELPER"],
        subhelper = L["RUNTIME_ONBOARDING_STEP_INTERRUPT_SUBHELPER"],
    },
    {
        key = "combatAlerts",
        title = L["RUNTIME_ONBOARDING_STEP_COMBAT_TITLE"],
        helper = L["RUNTIME_ONBOARDING_STEP_COMBAT_HELPER"],
        subhelper = L["RUNTIME_ONBOARDING_STEP_COMBAT_SUBHELPER"],
    },
    {
        key = "dock",
        title = L["RUNTIME_ONBOARDING_STEP_DOCK_TITLE"],
        helper = L["RUNTIME_ONBOARDING_STEP_DOCK_HELPER"],
        subhelper = L["RUNTIME_ONBOARDING_STEP_DOCK_SUBHELPER"],
    },
}

local PA_ONBOARDING_D4_STEP_BY_KEY = {}
for stepIndex, descriptor in ipairs(PA_ONBOARDING_D4_STEPS) do
    descriptor.index = stepIndex
    PA_ONBOARDING_D4_STEP_BY_KEY[descriptor.key] = descriptor
end

local function PA_OnboardingD4DeepCopy(value)
    if type(value) ~= "table" then
        return value
    end
    local copy = {}
    for key, entry in pairs(value) do
        copy[PA_OnboardingD4DeepCopy(key)] = PA_OnboardingD4DeepCopy(entry)
    end
    return copy
end

local function PA_OnboardingD4Round(value)
    return math.floor(tonumber(value) or 0)
end

local function PA_OnboardingD4GetRuntime(authority)
    authority._onboardingD4 = type(authority._onboardingD4) == "table" and authority._onboardingD4 or {}
    local runtime = authority._onboardingD4
    runtime.active = runtime.active == true
    runtime.stepIndex = tonumber(runtime.stepIndex) or nil
    runtime.currentStepKey = type(runtime.currentStepKey) == "string" and runtime.currentStepKey or nil
    runtime.movedPositions = type(runtime.movedPositions) == "table" and runtime.movedPositions or {}
    runtime.stepEntryPositions = type(runtime.stepEntryPositions) == "table" and runtime.stepEntryPositions or {}
    runtime.recoverySession = type(runtime.recoverySession) == "table" and runtime.recoverySession or nil
    return runtime
end

local function PA_OnboardingD4GetMovedKeys(runtime)
    local keys = {}
    for key, value in pairs(runtime and runtime.movedPositions or {}) do
        if type(value) == "table" then
            keys[#keys + 1] = key
        end
    end
    table.sort(keys)
    return keys
end

local function PA_OnboardingD4SetMouseDisabled(frame)
    if not frame then
        return
    end
    if frame.EnableMouse then
        frame:EnableMouse(false)
    end
    if frame.SetMouseClickEnabled then
        frame:SetMouseClickEnabled(false)
    end
    if frame.SetMouseMotionEnabled then
        frame:SetMouseMotionEnabled(false)
    end
end

local function PA_OnboardingD4BuildModuleLanePosition(moduleKey)
    local y = PA_ONBOARDING_D4_MODULE_OFFSET_Y
    if moduleKey == "combatAlerts" then
        y = PA_ONBOARDING_D4_MODULE_OFFSET_Y
    end
    return {
        x = PA_ONBOARDING_D4_MODULE_OFFSET_X,
        y = y,
    }
end

local function PA_OnboardingD4GetCardWidthForStep(step)
    if step and step.index == #PA_ONBOARDING_D4_STEPS then
        return PA_ONBOARDING_D4_CARD_FINAL_WIDTH
    end
    return PA_ONBOARDING_D4_CARD_WIDTH
end

local function PA_OnboardingD4ApplyCardGeometry(card, width)
    if not card then
        return
    end
    local resolvedWidth = math.floor(tonumber(width) or PA_ONBOARDING_D4_CARD_WIDTH)
    local centerOffsetX = PA_ONBOARDING_D4_CARD_OFFSET_X + math.floor((resolvedWidth - PA_ONBOARDING_D4_CARD_WIDTH) / 2)
    card:SetSize(resolvedWidth, PA_ONBOARDING_D4_CARD_HEIGHT)
    card:ClearAllPoints()
    card:SetPoint("CENTER", UIParent, "CENTER", centerOffsetX, PA_ONBOARDING_D4_CARD_OFFSET_Y)
end

function PortalAuthority:IsOnboardingD4Active()
    return PA_OnboardingD4GetRuntime(self).active == true
end

function PortalAuthority:CanStartOnboardingD4()
    if not PA_IsOnboardingD4Enabled() then
        return false, L["RUNTIME_ONBOARDING_GUIDED_SETUP_UNAVAILABLE_BUILD"]
    end
    if (InCombatLockdown and InCombatLockdown()) or self:IsPlayerInCombat() then
        return false, L["RUNTIME_ONBOARDING_GUIDED_SETUP_UNAVAILABLE_COMBAT"]
    end
    if PA_IsChallengeActive and PA_IsChallengeActive() then
        return false, L["RUNTIME_ONBOARDING_GUIDED_SETUP_UNAVAILABLE_ACTIVE_MPLUS"]
    end
    return true, L["COMMON_READY"]
end

function PortalAuthority:GetOnboardingD4Step(stepRef)
    if type(stepRef) == "string" then
        return PA_ONBOARDING_D4_STEP_BY_KEY[stepRef]
    end
    local index = math.floor(tonumber(stepRef) or 0)
    if index < 1 or index > #PA_ONBOARDING_D4_STEPS then
        return nil
    end
    return PA_ONBOARDING_D4_STEPS[index]
end

function PortalAuthority:HideSettingsPanelForOnboardingD4()
    local panel = SettingsPanel
    if not panel or not panel.IsShown or not panel:IsShown() then
        return
    end
    if HideUIPanel then
        HideUIPanel(panel)
        return
    end
    panel:Hide()
end

function PortalAuthority:EnsureOnboardingD4HighlightFrame()
    local runtime = PA_OnboardingD4GetRuntime(self)
    if runtime.highlight and runtime.highlight.SetBackdrop then
        return runtime.highlight
    end

    local highlight = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    runtime.highlight = highlight
    highlight:SetFrameStrata("TOOLTIP")
    highlight:SetFrameLevel(8)
    highlight:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 2,
    })
    highlight:SetBackdropColor(0.52, 0.40, 0.74, 0.04)
    highlight:SetBackdropBorderColor(0.60, 0.46, 0.82, 0.55)
    highlight:Hide()
    PA_OnboardingD4SetMouseDisabled(highlight)
    return highlight
end

function PortalAuthority:HideOnboardingD4Highlight()
    local runtime = PA_OnboardingD4GetRuntime(self)
    if runtime.highlight and runtime.highlight.Hide then
        runtime.highlight:Hide()
    end
    runtime.highlightTarget = nil
end

function PortalAuthority:BindOnboardingD4HighlightToFrame(targetFrame)
    local highlight = self:EnsureOnboardingD4HighlightFrame()
    local runtime = PA_OnboardingD4GetRuntime(self)
    if not highlight or not targetFrame then
        self:HideOnboardingD4Highlight()
        return
    end

    runtime.highlightTarget = targetFrame
    highlight:ClearAllPoints()
    highlight:SetPoint("TOPLEFT", targetFrame, "TOPLEFT", -8, 8)
    highlight:SetPoint("BOTTOMRIGHT", targetFrame, "BOTTOMRIGHT", 8, -8)
    highlight:Show()
    if highlight.Raise then
        highlight:Raise()
    end
end

function PortalAuthority:EnsureOnboardingD4Card()
    local runtime = PA_OnboardingD4GetRuntime(self)
    if runtime.card then
        return runtime.card
    end

    local card = CreateFrame("Frame", "PortalAuthorityOnboardingD4Card", UIParent, "BackdropTemplate")
    runtime.card = card
    PA_OnboardingD4ApplyCardGeometry(card, PA_ONBOARDING_D4_CARD_WIDTH)
    card:SetFrameStrata("DIALOG")
    card:SetFrameLevel(128)
    card:SetToplevel(true)
    card:SetClampedToScreen(true)
    card:EnableMouse(true)
    card:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    card:SetBackdropColor(0.04, 0.05, 0.07, 0.975)
    card:SetBackdropBorderColor(0.19, 0.21, 0.27, 0.94)
    card:SetScript("OnMouseDown", function() end)
    card:SetScript("OnMouseUp", function() end)
    card:Hide()

    local shadow = CreateFrame("Frame", nil, card, "BackdropTemplate")
    shadow:SetPoint("TOPLEFT", card, "TOPLEFT", -10, 10)
    shadow:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", 10, -10)
    shadow:SetFrameStrata(card:GetFrameStrata())
    shadow:SetFrameLevel(math.max(card:GetFrameLevel() - 2, 1))
    shadow:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
    })
    shadow:SetBackdropColor(0, 0, 0, 0.20)
    PA_OnboardingD4SetMouseDisabled(shadow)
    card._shadow = shadow

    local surfaceDepth = card:CreateTexture(nil, "BACKGROUND", nil, 1)
    surfaceDepth:SetPoint("TOPLEFT", card, "TOPLEFT", 1, -1)
    surfaceDepth:SetPoint("TOPRIGHT", card, "TOPRIGHT", -1, -1)
    surfaceDepth:SetHeight(88)
    -- Some WoW clients do not expose gradient helpers on textures, so keep this
    -- as a simple top wash instead of relying on SetGradientAlpha.
    surfaceDepth:SetTexture("Interface\\Buttons\\WHITE8x8")
    surfaceDepth:SetVertexColor(0.12, 0.13, 0.18, 0.08)
    card._surfaceDepth = surfaceDepth

    local badge = card:CreateTexture(nil, "ARTWORK")
    badge:SetTexture("Interface\\AddOns\\PortalAuthority\\Media\\Images\\PA-Large-Transparent.png")
    badge:SetSize(18, 18)
    badge:SetPoint("TOPLEFT", card, "TOPLEFT", 18, -16)
    card._badge = badge

    local header = card:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
    PA_ApplyOnboardingLocalizedTextFont(self, header, 11)
    header:SetPoint("LEFT", badge, "RIGHT", 8, 0)
    header:SetTextColor(0.75, 0.75, 0.79, 1.0)
    header:SetText(L["COMMON_GUIDED_SETUP"])
    card._header = header

    local stepCounter = card:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    PA_ApplyOnboardingLocalizedTextFont(self, stepCounter, 11)
    stepCounter:SetPoint("TOPRIGHT", card, "TOPRIGHT", -18, -16)
    stepCounter:SetJustifyH("RIGHT")
    stepCounter:SetTextColor(0.60, 0.61, 0.66, 1.0)
    card._stepCounter = stepCounter

    local title = card:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    PA_ApplyOnboardingLocalizedTextFont(self, title, 23)
    title:SetPoint("TOPLEFT", card, "TOPLEFT", 18, -52)
    title:SetPoint("TOPRIGHT", card, "TOPRIGHT", -18, -52)
    title:SetJustifyH("LEFT")
    title:SetWordWrap(false)
    title:SetTextColor(0.97, 0.97, 0.98, 1.0)
    card._title = title

    local helper = card:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
    PA_ApplyOnboardingLocalizedTextFont(self, helper, 14)
    helper:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -12)
    helper:SetPoint("TOPRIGHT", title, "BOTTOMRIGHT", 0, -12)
    helper:SetJustifyH("LEFT")
    helper:SetWordWrap(true)
    helper:SetSpacing(1)
    helper:SetTextColor(0.90, 0.89, 0.91, 1.0)
    card._helper = helper

    local subhelper = card:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    PA_ApplyOnboardingLocalizedTextFont(self, subhelper, 13)
    subhelper:SetPoint("TOPLEFT", helper, "BOTTOMLEFT", 0, -5)
    subhelper:SetPoint("TOPRIGHT", helper, "BOTTOMRIGHT", 0, -5)
    subhelper:SetJustifyH("LEFT")
    subhelper:SetWordWrap(true)
    subhelper:SetSpacing(1)
    subhelper:SetTextColor(0.70, 0.71, 0.76, 1.0)
    card._subhelper = subhelper

    local settingsNote = card:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    PA_ApplyOnboardingLocalizedTextFont(self, settingsNote, 11)
    settingsNote:SetPoint("BOTTOMLEFT", card, "BOTTOMLEFT", 18, 90)
    settingsNote:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", -18, 90)
    settingsNote:SetJustifyH("LEFT")
    settingsNote:SetWordWrap(true)
    settingsNote:SetSpacing(1)
    settingsNote:SetTextColor(0.51, 0.52, 0.57, 1.0)
    settingsNote:SetText(PA_ONBOARDING_D4_SETTINGS_NOTE)
    settingsNote:Hide()
    card._settingsNote = settingsNote

    local function createButton(width, label, style)
        local button = CreateFrame("Button", nil, card, "BackdropTemplate")
        button:SetSize(width, 30)
        button:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            edgeSize = 1,
        })
        button._style = tostring(style or "secondary")

        local text = button:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        PA_ApplyOnboardingLocalizedTextFont(PortalAuthority, text, 12)
        text:SetPoint("CENTER")
        text:SetText(label)
        button._label = text

        local function refreshVisual(selfBtn, pressed)
            if not selfBtn:IsEnabled() then
                if selfBtn._style == "ghost" then
                    selfBtn:SetBackdropColor(0.05, 0.06, 0.08, 0.12)
                    selfBtn:SetBackdropBorderColor(0.18, 0.20, 0.25, 0.26)
                    selfBtn._label:SetTextColor(0.41, 0.42, 0.46, 1.0)
                elseif selfBtn._style == "primary" then
                    selfBtn:SetBackdropColor(0.10, 0.09, 0.15, 0.52)
                    selfBtn:SetBackdropBorderColor(0.26, 0.22, 0.36, 0.40)
                    selfBtn._label:SetTextColor(0.53, 0.52, 0.58, 1.0)
                else
                    selfBtn:SetBackdropColor(0.08, 0.09, 0.12, 0.46)
                    selfBtn:SetBackdropBorderColor(0.18, 0.20, 0.25, 0.36)
                    selfBtn._label:SetTextColor(0.49, 0.49, 0.53, 1.0)
                end
                return
            end

            if selfBtn._style == "primary" then
                if pressed then
                    selfBtn:SetBackdropColor(0.18, 0.14, 0.27, 0.98)
                    selfBtn:SetBackdropBorderColor(0.54, 0.40, 0.74, 0.92)
                elseif selfBtn._hover then
                    selfBtn:SetBackdropColor(0.17, 0.13, 0.25, 0.98)
                    selfBtn:SetBackdropBorderColor(0.49, 0.37, 0.68, 0.88)
                else
                    selfBtn:SetBackdropColor(0.14, 0.11, 0.21, 0.97)
                    selfBtn:SetBackdropBorderColor(0.42, 0.31, 0.58, 0.82)
                end
                selfBtn._label:SetTextColor(0.98, 0.97, 0.98, 1.0)
                return
            end

            if selfBtn._style == "ghost" then
                if pressed then
                    selfBtn:SetBackdropColor(0.08, 0.09, 0.12, 0.30)
                    selfBtn:SetBackdropBorderColor(0.24, 0.26, 0.34, 0.70)
                elseif selfBtn._hover then
                    selfBtn:SetBackdropColor(0.06, 0.07, 0.10, 0.24)
                    selfBtn:SetBackdropBorderColor(0.22, 0.25, 0.32, 0.64)
                else
                    selfBtn:SetBackdropColor(0.05, 0.06, 0.08, 0.16)
                    selfBtn:SetBackdropBorderColor(0.19, 0.21, 0.27, 0.52)
                end
                selfBtn._label:SetTextColor(0.80, 0.80, 0.84, 1.0)
                return
            end

            if pressed then
                selfBtn:SetBackdropColor(0.10, 0.11, 0.15, 0.96)
                selfBtn:SetBackdropBorderColor(0.27, 0.30, 0.38, 0.84)
            elseif selfBtn._hover then
                selfBtn:SetBackdropColor(0.09, 0.10, 0.14, 0.96)
                selfBtn:SetBackdropBorderColor(0.24, 0.27, 0.34, 0.82)
            else
                selfBtn:SetBackdropColor(0.07, 0.08, 0.11, 0.94)
                selfBtn:SetBackdropBorderColor(0.21, 0.23, 0.30, 0.76)
            end
            selfBtn._label:SetTextColor(0.87, 0.87, 0.90, 1.0)
        end

        button._refreshVisual = refreshVisual
        button:SetScript("OnEnter", function(selfBtn)
            selfBtn._hover = true
            selfBtn:_refreshVisual(false)
        end)
        button:SetScript("OnLeave", function(selfBtn)
            selfBtn._hover = nil
            selfBtn:_refreshVisual(false)
        end)
        button:SetScript("OnMouseDown", function(selfBtn)
            selfBtn:_refreshVisual(true)
        end)
        button:SetScript("OnMouseUp", function(selfBtn)
            selfBtn:_refreshVisual(false)
        end)
        button:SetScript("OnShow", function(selfBtn)
            selfBtn:_refreshVisual(false)
        end)
        button:SetScript("OnEnable", function(selfBtn)
            selfBtn:_refreshVisual(false)
        end)
        button:SetScript("OnDisable", function(selfBtn)
            selfBtn:_refreshVisual(false)
        end)
        button:Enable()
        button:_refreshVisual(false)
        return button
    end

    local function createQuietAction(label, width, fontSize, justifyH)
        local button = CreateFrame("Button", nil, card)
        button:SetSize(width or 84, 18)
        local text = button:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        PA_ApplyOnboardingLocalizedTextFont(PortalAuthority, text, fontSize or 12)
        local justification = tostring(justifyH or "CENTER"):upper()
        if justification == "RIGHT" then
            text:SetPoint("RIGHT", button, "RIGHT", 0, 0)
            text:SetJustifyH("RIGHT")
        elseif justification == "LEFT" then
            text:SetPoint("LEFT", button, "LEFT", 0, 0)
            text:SetJustifyH("LEFT")
        else
            text:SetPoint("CENTER")
            text:SetJustifyH("CENTER")
        end
        text:SetText(label)
        text:SetTextColor(0.63, 0.63, 0.67, 1.0)
        button._label = text
        button:SetHitRectInsets(-8, -8, -5, -5)
        button:SetScript("OnEnter", function(selfBtn)
            selfBtn._label:SetTextColor(0.82, 0.82, 0.86, 1.0)
        end)
        button:SetScript("OnLeave", function(selfBtn)
            selfBtn._label:SetTextColor(0.63, 0.63, 0.67, 1.0)
        end)
        return button
    end

    local backButton = createButton(82, L["COMMON_BACK"], "ghost")
    backButton:SetPoint("BOTTOMLEFT", card, "BOTTOMLEFT", 18, 50)
    card._backButton = backButton

    local nextButton = createButton(118, L["COMMON_NEXT"], "primary")
    nextButton:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", -18, 50)
    card._nextButton = nextButton

    local finishButton = createButton(118, L["COMMON_FINISH"], "primary")
    finishButton:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", -18, 50)
    finishButton:Hide()
    card._finishButton = finishButton

    local skipButton = createQuietAction(L["COMMON_SKIP"])
    skipButton:SetPoint("BOTTOM", card, "BOTTOM", 0, 18)
    card._skipButton = skipButton

    local dockSettingsButton = createQuietAction(L["COMMON_OPEN_DOCK_SETTINGS"], 138, 11, "RIGHT")
    dockSettingsButton:SetPoint("RIGHT", card, "RIGHT", -18, 0)
    dockSettingsButton:SetPoint("CENTER", skipButton, "CENTER", 105, 0)
    dockSettingsButton:Hide()
    card._dockSettingsButton = dockSettingsButton

    backButton:SetScript("OnClick", function()
        if PortalAuthority then
            PortalAuthority:ShowOnboardingD4Step((PA_OnboardingD4GetRuntime(PortalAuthority).stepIndex or 1) - 1)
        end
    end)
    nextButton:SetScript("OnClick", function()
        if PortalAuthority then
            PortalAuthority:ShowOnboardingD4Step((PA_OnboardingD4GetRuntime(PortalAuthority).stepIndex or 1) + 1)
        end
    end)
    finishButton:SetScript("OnClick", function()
        if PortalAuthority then
            local ok, response = PortalAuthority:CompleteOnboardingD4Session({
                preserveMovedPositions = true,
                exitReason = "finish",
            })
            if not ok then
                PA_PrintPortalAuthorityMessage(response or L["RUNTIME_ONBOARDING_GUIDED_SETUP_CLEANUP_FAILED"])
            end
        end
    end)
    dockSettingsButton:SetScript("OnClick", function()
        if PortalAuthority then
            local ok, response = PortalAuthority:CompleteOnboardingD4Session({
                preserveMovedPositions = true,
                openDockSettings = true,
                exitReason = "open-dock-settings",
            })
            if not ok then
                PA_PrintPortalAuthorityMessage(response or L["RUNTIME_ONBOARDING_GUIDED_SETUP_CLEANUP_FAILED"])
            end
        end
    end)
    skipButton:SetScript("OnClick", function()
        if PortalAuthority then
            local ok, response = PortalAuthority:CompleteOnboardingD4Session({
                preserveMovedPositions = false,
                exitReason = "skip",
            })
            if not ok then
                PA_PrintPortalAuthorityMessage(response or L["RUNTIME_ONBOARDING_GUIDED_SETUP_CLEANUP_FAILED"])
            end
        end
    end)

    card:SetScript("OnShow", function(frame)
        frame:SetAlpha(1)
        if frame.Raise then
            frame:Raise()
        end
    end)

    return card
end

function PortalAuthority:HideOnboardingD4Card()
    local runtime = PA_OnboardingD4GetRuntime(self)
    if runtime.card and runtime.card.Hide then
        runtime.card:Hide()
    end
end

function PortalAuthority:FocusOnboardingD4Card()
    local card = self:EnsureOnboardingD4Card()
    if not card then
        return
    end
    card:Show()
    if card.Raise then
        card:Raise()
    end
end

function PortalAuthority:GetOnboardingD4ModulePosition(moduleKey)
    local db = PortalAuthorityDB or self.defaults or {}
    db.modules = db.modules or {}
    db.modules.timers = db.modules.timers or {}
    db.modules.interruptTracker = db.modules.interruptTracker or {}

    if moduleKey == "dock" then
        return {
            x = PA_OnboardingD4Round(db.dockX),
            y = PA_OnboardingD4Round(db.dockY),
        }
    end
    if moduleKey == "timers" then
        return {
            x = PA_OnboardingD4Round(db.modules.timers.x),
            y = PA_OnboardingD4Round(db.modules.timers.y),
        }
    end
    if moduleKey == "interruptTracker" then
        return {
            x = PA_OnboardingD4Round(db.modules.interruptTracker.x),
            y = PA_OnboardingD4Round(db.modules.interruptTracker.y),
        }
    end
    if moduleKey == "combatAlerts" then
        return {
            x = PA_OnboardingD4Round(db.deathAlertX),
            y = PA_OnboardingD4Round(db.deathAlertY),
        }
    end
    return nil
end

function PortalAuthority:GetOnboardingD4LiveModulePosition(moduleKey)
    local frame = self:GetOnboardingD4ActiveModuleFrame(moduleKey)
    if not frame or not frame.GetCenter then
        return nil
    end

    local cx, cy = frame:GetCenter()
    local ux, uy = nil, nil
    if UIParent and UIParent.GetCenter then
        ux, uy = UIParent:GetCenter()
    end
    if not cx or not cy or not ux or not uy then
        return nil
    end

    return {
        x = PA_OnboardingD4Round((cx - ux) + 0.5),
        y = PA_OnboardingD4Round((cy - uy) + 0.5),
    }
end

function PortalAuthority:GetOnboardingD4ActiveModuleFrame(moduleKey)
    if moduleKey == "dock" then
        return self.dockFrame
    end

    local modulesApi = self.Modules
    if moduleKey == "timers" then
        local module = modulesApi and modulesApi.registry and modulesApi.registry.timers or nil
        return module and (module.frame or module.mainFrame) or nil
    end
    if moduleKey == "interruptTracker" then
        local module = modulesApi and modulesApi.registry and modulesApi.registry.interruptTracker or nil
        return module and module.frame or nil
    end
    if moduleKey == "combatAlerts" then
        return self.DeathAlerts and self.DeathAlerts.frame or nil
    end
    return nil
end

function PortalAuthority:CaptureOnboardingD4RecoverySession()
    self:EnsureDB()
    local db = PortalAuthorityDB or self.defaults or {}
    db.modules = db.modules or {}
    db.modules.timers = db.modules.timers or {}
    db.modules.interruptTracker = db.modules.interruptTracker or {}

    return {
        version = 1,
        currentStepKey = nil,
        modules = {
            timers = {
                enabled = db.modules.timers.enabled ~= false,
                locked = db.modules.timers.locked ~= false,
                x = PA_OnboardingD4Round(db.modules.timers.x),
                y = PA_OnboardingD4Round(db.modules.timers.y),
                testMode = self.Modules and self.Modules.timersTestMode == true or false,
            },
            interruptTracker = {
                enabled = db.modules.interruptTracker.enabled ~= false,
                locked = db.modules.interruptTracker.locked ~= false,
                x = PA_OnboardingD4Round(db.modules.interruptTracker.x),
                y = PA_OnboardingD4Round(db.modules.interruptTracker.y),
            },
            combatAlerts = {
                enabled = self:IsCombatAlertsEnabled(),
                locked = db.deathAlertLocked ~= false,
                x = PA_OnboardingD4Round(db.deathAlertX),
                y = PA_OnboardingD4Round(db.deathAlertY),
                testMode = self.DeathAlerts_IsTestModeActive and self:DeathAlerts_IsTestModeActive() or false,
            },
            dock = {
                enabled = db.dockEnabled ~= false,
                locked = db.dockLocked ~= false,
                x = PA_OnboardingD4Round(db.dockX),
                y = PA_OnboardingD4Round(db.dockY),
            },
        },
    }
end

function PortalAuthority:PersistOnboardingD4RecoverySession(stepKey)
    local runtime = PA_OnboardingD4GetRuntime(self)
    if type(runtime.recoverySession) ~= "table" then
        return nil
    end
    if type(stepKey) == "string" and stepKey ~= "" then
        runtime.recoverySession.currentStepKey = stepKey
    end
    if self.Profiles_SetOnboardingD4Session then
        self:Profiles_SetOnboardingD4Session(runtime.recoverySession)
    end
    return runtime.recoverySession
end

function PortalAuthority:RefreshOnboardingD4Card()
    local runtime = PA_OnboardingD4GetRuntime(self)
    local card = self:EnsureOnboardingD4Card()
    local step = self:GetOnboardingD4Step(runtime.stepIndex)
    if not card or not step then
        return
    end

    PA_OnboardingD4ApplyCardGeometry(card, PA_OnboardingD4GetCardWidthForStep(step))

    card._stepCounter:SetText(string.format(L["RUNTIME_ONBOARDING_GUIDED_SETUP_STEP_FORMAT"], step.index, #PA_ONBOARDING_D4_STEPS))
    card._title:SetText(step.title)
    card._helper:SetText(step.helper)
    card._subhelper:SetText(step.subhelper)

    local isFinalStep = step.index == #PA_ONBOARDING_D4_STEPS
    local isFirstStep = step.index == 1

    card._settingsNote:Hide()

    card._backButton:Show()
    card._backButton:ClearAllPoints()
    card._backButton:SetPoint("BOTTOMLEFT", card, "BOTTOMLEFT", 18, 50)
    if isFirstStep then
        card._backButton:Disable()
    else
        card._backButton:Enable()
    end

    if isFinalStep then
        card._nextButton:Hide()

        card._finishButton:ClearAllPoints()
        card._finishButton:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", -18, 50)
        card._finishButton:Show()

        card._dockSettingsButton:ClearAllPoints()
        card._dockSettingsButton:SetPoint("RIGHT", card, "RIGHT", -18, 0)
        card._dockSettingsButton:SetPoint("CENTER", card._skipButton, "CENTER", 105, 0)
        card._dockSettingsButton:Show()
    else
        card._nextButton:ClearAllPoints()
        card._nextButton:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", -18, 50)
        card._nextButton:Show()

        card._finishButton:Hide()
        card._dockSettingsButton:Hide()
    end

    card._skipButton:Show()
end

function PortalAuthority:SyncOnboardingD4ActiveStepPosition()
    local runtime = PA_OnboardingD4GetRuntime(self)
    if not runtime.active or not runtime.currentStepKey then
        return nil
    end

    local currentPosition = self:GetOnboardingD4LiveModulePosition(runtime.currentStepKey)
    if not currentPosition then
        currentPosition = self:GetOnboardingD4ModulePosition(runtime.currentStepKey)
    end
    local entryPosition = runtime.stepEntryPositions[runtime.currentStepKey]
    if not currentPosition or type(entryPosition) ~= "table" then
        return currentPosition
    end

    if currentPosition.x ~= PA_OnboardingD4Round(entryPosition.x) or currentPosition.y ~= PA_OnboardingD4Round(entryPosition.y) then
        runtime.movedPositions[runtime.currentStepKey] = {
            x = currentPosition.x,
            y = currentPosition.y,
        }
    end
    return currentPosition
end

function PortalAuthority:ApplyOnboardingD4DockState(stateSnapshot, positionOverride)
    self:EnsureDB()
    local db = PortalAuthorityDB or self.defaults or {}
    local pos = positionOverride or stateSnapshot or {}

    if self.InitializeDock and not self.dockFrame then
        self:InitializeDock()
    end

    db.dockEnabled = stateSnapshot.enabled ~= false
    db.dockLocked = stateSnapshot.locked ~= false
    db.dockX = PA_OnboardingD4Round(pos.x)
    db.dockY = PA_OnboardingD4Round(pos.y)

    if self.ApplyDockPosition then
        self:ApplyDockPosition()
    end
    if self.UpdateDockVisibility then
        self:UpdateDockVisibility(true)
    elseif self.RefreshDockVisibility then
        self:RefreshDockVisibility(true)
    end
    if self.UpdateDockMovableState then
        self:UpdateDockMovableState()
    end
    if self.ApplyDockCombatVisibilityDriver then
        self:ApplyDockCombatVisibilityDriver()
    end
end

function PortalAuthority:ApplyOnboardingD4TimersState(stateSnapshot, positionOverride)
    self:EnsureDB()
    local db = PortalAuthorityDB or self.defaults or {}
    db.modules = db.modules or {}
    db.modules.timers = db.modules.timers or {}
    local timersDb = db.modules.timers
    local pos = positionOverride or stateSnapshot or {}

    timersDb.enabled = stateSnapshot.enabled ~= false
    timersDb.locked = stateSnapshot.locked ~= false
    timersDb.x = PA_OnboardingD4Round(pos.x)
    timersDb.y = PA_OnboardingD4Round(pos.y)

    local modulesApi = self.Modules
    local module = modulesApi and modulesApi.registry and modulesApi.registry.timers or nil
    if module and timersDb.enabled and module.Initialize and not (module.frame or module.mainFrame) then
        module:Initialize()
    end
    if module and module.ApplyPosition then
        module:ApplyPosition()
    end
    if modulesApi and modulesApi.NotifyPositionChanged then
        modulesApi:NotifyPositionChanged("timers", timersDb.x, timersDb.y)
    end
    if module and module.SetUnlocked then
        module:SetUnlocked(not timersDb.locked)
    end
    if modulesApi and modulesApi.SetTimersTestMode then
        modulesApi:SetTimersTestMode(stateSnapshot.testMode == true)
    end
    if module and module.EvaluateVisibility then
        module:EvaluateVisibility("onboarding-d4")
    end
    if stateSnapshot.testMode == true and module and module.Tick then
        module:Tick()
    end
end

function PortalAuthority:ApplyOnboardingD4InterruptTrackerState(stateSnapshot, positionOverride)
    self:EnsureDB()
    local db = PortalAuthorityDB or self.defaults or {}
    db.modules = db.modules or {}
    db.modules.interruptTracker = db.modules.interruptTracker or {}
    local interruptDb = db.modules.interruptTracker
    local pos = positionOverride or stateSnapshot or {}

    interruptDb.enabled = stateSnapshot.enabled ~= false
    interruptDb.locked = stateSnapshot.locked ~= false
    interruptDb.x = PA_OnboardingD4Round(pos.x)
    interruptDb.y = PA_OnboardingD4Round(pos.y)

    local modulesApi = self.Modules
    local module = modulesApi and modulesApi.registry and modulesApi.registry.interruptTracker or nil
    if module and interruptDb.enabled and module.Initialize and not module.frame then
        module:Initialize()
    end
    if module and module.ApplyPosition then
        module:ApplyPosition()
    end
    if modulesApi and modulesApi.NotifyPositionChanged then
        modulesApi:NotifyPositionChanged("interruptTracker", interruptDb.x, interruptDb.y)
    end
    if module and module.SetUnlocked then
        module:SetUnlocked(not interruptDb.locked)
    end
    if module and module.EvaluateVisibility then
        module:EvaluateVisibility("onboarding-d4")
    end
end

function PortalAuthority:ApplyOnboardingD4CombatAlertsState(stateSnapshot, positionOverride)
    self:EnsureDB()
    local db = PortalAuthorityDB or self.defaults or {}
    local pos = positionOverride or stateSnapshot or {}

    if self.InitializeDeathAlerts and not (self.DeathAlerts and self.DeathAlerts.frame) then
        self:InitializeDeathAlerts()
    end

    db.combatAlertsEnabled = stateSnapshot.enabled ~= false
    db.deathAlertLocked = stateSnapshot.locked ~= false
    db.deathAlertX = PA_OnboardingD4Round(pos.x)
    db.deathAlertY = PA_OnboardingD4Round(pos.y)

    if self.ApplyCombatAlertsSettings then
        self:ApplyCombatAlertsSettings()
    end

    local shouldEnableTestMode = stateSnapshot.testMode == true
    local currentTestMode = self.DeathAlerts_IsTestModeActive and self:DeathAlerts_IsTestModeActive() or false
    if shouldEnableTestMode ~= currentTestMode and self.DeathAlerts_ToggleTestMode then
        self:DeathAlerts_ToggleTestMode()
    elseif shouldEnableTestMode and self.DeathAlerts_RefreshTestMode then
        self:DeathAlerts_RefreshTestMode()
    end
end

function PortalAuthority:ApplyOnboardingD4ModuleState(moduleKey, stateSnapshot, positionOverride)
    if moduleKey == "dock" then
        self:ApplyOnboardingD4DockState(stateSnapshot, positionOverride)
        return
    end
    if moduleKey == "timers" then
        self:ApplyOnboardingD4TimersState(stateSnapshot, positionOverride)
        return
    end
    if moduleKey == "interruptTracker" then
        self:ApplyOnboardingD4InterruptTrackerState(stateSnapshot, positionOverride)
        return
    end
    if moduleKey == "combatAlerts" then
        self:ApplyOnboardingD4CombatAlertsState(stateSnapshot, positionOverride)
    end
end

function PortalAuthority:ApplyOnboardingD4StepState(moduleKey, positionOverride)
    if moduleKey == "dock" then
        self:ApplyOnboardingD4DockState({
            enabled = true,
            locked = false,
        }, positionOverride)
        return
    end
    if moduleKey == "timers" then
        self:ApplyOnboardingD4TimersState({
            enabled = true,
            locked = false,
            testMode = true,
        }, positionOverride)
        return
    end
    if moduleKey == "interruptTracker" then
        self:ApplyOnboardingD4InterruptTrackerState({
            enabled = true,
            locked = false,
        }, positionOverride)
        return
    end
    if moduleKey == "combatAlerts" then
        self:ApplyOnboardingD4CombatAlertsState({
            enabled = true,
            locked = false,
            testMode = true,
        }, positionOverride)
    end
end

function PortalAuthority:RestoreOnboardingD4Session(session, movedPositions)
    local snapshots = session and session.modules or nil
    if type(snapshots) ~= "table" then
        return false, L["RUNTIME_ONBOARDING_GUIDED_SETUP_RECOVERY_UNAVAILABLE"]
    end

    self:ApplyOnboardingD4ModuleState("timers", snapshots.timers or {}, movedPositions and movedPositions.timers or nil)
    self:ApplyOnboardingD4ModuleState("interruptTracker", snapshots.interruptTracker or {}, movedPositions and movedPositions.interruptTracker or nil)
    self:ApplyOnboardingD4ModuleState("combatAlerts", snapshots.combatAlerts or {}, movedPositions and movedPositions.combatAlerts or nil)
    self:ApplyOnboardingD4ModuleState("dock", snapshots.dock or {}, movedPositions and movedPositions.dock or nil)

    if self.RefreshOptionsPanels then
        self:RefreshOptionsPanels()
    end
    if self.UpdateMoveHintTickerState then
        self:UpdateMoveHintTickerState()
    end

    return true
end

function PortalAuthority:ResetOnboardingD4RuntimeState()
    local runtime = PA_OnboardingD4GetRuntime(self)
    local previousDeferState = runtime.previousDeferSharedMarkerTickerRefresh == true

    runtime.active = false
    runtime.stepIndex = nil
    runtime.currentStepKey = nil
    runtime.movedPositions = {}
    runtime.stepEntryPositions = {}
    runtime.recoverySession = nil
    runtime.previousDeferSharedMarkerTickerRefresh = nil

    self:HideOnboardingD4Highlight()
    self:HideOnboardingD4Card()

    self._deferSharedMarkerTickerRefresh = previousDeferState and true or nil
    if self.UpdateMoveHintTickerState then
        self:UpdateMoveHintTickerState()
    end
end

function PortalAuthority:CompleteOnboardingD4Session(config)
    config = type(config) == "table" and config or {}

    local runtime = PA_OnboardingD4GetRuntime(self)
    if runtime.active then
        self:SyncOnboardingD4ActiveStepPosition()
    end

    local session = config.session
    if type(session) ~= "table" then
        session = runtime.recoverySession
    end
    if type(session) ~= "table" and self.Profiles_GetOnboardingD4Session then
        session = self:Profiles_GetOnboardingD4Session()
    end

    local movedPositions = nil
    if config.preserveMovedPositions == true then
        movedPositions = PA_OnboardingD4DeepCopy(runtime.movedPositions)
    end

    if config.strictRestore == true and type(session) ~= "table" then
        return false, L["RUNTIME_ONBOARDING_GUIDED_SETUP_RECOVERY_UNAVAILABLE"]
    end

    if type(session) == "table" then
        local ok, err = self:RestoreOnboardingD4Session(session, movedPositions)
        if not ok and config.strictRestore == true then
            return false, err or L["RUNTIME_ONBOARDING_GUIDED_SETUP_CLEANUP_FAILED"]
        end
    end

    if self.Profiles_ClearOnboardingD4Session then
        self:Profiles_ClearOnboardingD4Session()
    end

    self:ResetOnboardingD4RuntimeState()

    if config.openDockSettings == true then
        local opened, openErr = self:OpenCustomSettingsSectionOrRoot("dock", "onboarding-d4-open-dock-settings")
        if not opened then
            return false, openErr or L["RUNTIME_ONBOARDING_GUIDED_SETUP_OPEN_DOCK_UNAVAILABLE"]
        end
    end

    if config.exitReason == "finish" then
        return true, L["RUNTIME_ONBOARDING_GUIDED_SETUP_FINISHED"]
    end
    if config.exitReason == "skip" then
        return true, L["RUNTIME_ONBOARDING_GUIDED_SETUP_SKIPPED"]
    end
    if config.exitReason == "open-dock-settings" then
        return true, L["RUNTIME_ONBOARDING_GUIDED_SETUP_FINISHED_OPENED_DOCK"]
    end
    return true, L["RUNTIME_ONBOARDING_GUIDED_SETUP_CLEANED_UP"]
end

function PortalAuthority:AbortOnboardingD4ForProfileMutation(reason)
    local runtime = PA_OnboardingD4GetRuntime(self)
    local persistedSession = self.Profiles_GetOnboardingD4Session and self:Profiles_GetOnboardingD4Session() or nil
    if not runtime.active and type(persistedSession) ~= "table" then
        return true
    end
    return self:CompleteOnboardingD4Session({
        preserveMovedPositions = false,
        exitReason = reason or "profile-operation",
        strictRestore = true,
        session = persistedSession,
    })
end

function PortalAuthority:ResolveAbandonedOnboardingD4Session(reason)
    local persistedSession = self.Profiles_GetOnboardingD4Session and self:Profiles_GetOnboardingD4Session() or nil
    if type(persistedSession) ~= "table" then
        return false, "No guided setup recovery session is present."
    end
    return self:CompleteOnboardingD4Session({
        preserveMovedPositions = false,
        exitReason = reason or "reload-recovery",
        session = persistedSession,
    })
end

function PortalAuthority:ShowOnboardingD4Step(stepRef)
    local runtime = PA_OnboardingD4GetRuntime(self)
    if not runtime.active then
        return false, L["RUNTIME_ONBOARDING_GUIDED_SETUP_NOT_ACTIVE"]
    end

    local step = self:GetOnboardingD4Step(stepRef)
    if not step then
        return false, L["RUNTIME_ONBOARDING_GUIDED_SETUP_STEP_UNAVAILABLE"]
    end

    if runtime.currentStepKey == step.key then
        self:RefreshOnboardingD4Card()
        self:FocusOnboardingD4Card()
        self:BindOnboardingD4HighlightToFrame(self:GetOnboardingD4ActiveModuleFrame(step.key))
        return true, string.format(L["RUNTIME_ONBOARDING_GUIDED_SETUP_REFOCUSED_FORMAT"], step.title)
    end

    self:SyncOnboardingD4ActiveStepPosition()

    if runtime.currentStepKey and runtime.recoverySession and runtime.recoverySession.modules then
        local previousSnapshot = runtime.recoverySession.modules[runtime.currentStepKey]
        local previousMovedPosition = runtime.movedPositions[runtime.currentStepKey]
        if previousSnapshot then
            self:ApplyOnboardingD4ModuleState(runtime.currentStepKey, previousSnapshot, previousMovedPosition)
        end
    end

    runtime.stepIndex = step.index
    runtime.currentStepKey = step.key
    self:PersistOnboardingD4RecoverySession(step.key)

    local position = runtime.movedPositions[step.key]
    if type(position) ~= "table" then
        position = PA_OnboardingD4BuildModuleLanePosition(step.key)
    end
    self:ApplyOnboardingD4StepState(step.key, position)
    runtime.stepEntryPositions[step.key] = {
        x = PA_OnboardingD4Round(position.x),
        y = PA_OnboardingD4Round(position.y),
    }

    self:RefreshOnboardingD4Card()
    self:FocusOnboardingD4Card()

    local targetFrame = self:GetOnboardingD4ActiveModuleFrame(step.key)
    self:BindOnboardingD4HighlightToFrame(targetFrame)
    if C_Timer and C_Timer.After then
        C_Timer.After(0, function()
            local currentRuntime = PA_OnboardingD4GetRuntime(PortalAuthority)
            if currentRuntime.active and currentRuntime.currentStepKey == step.key then
                PortalAuthority:BindOnboardingD4HighlightToFrame(PortalAuthority:GetOnboardingD4ActiveModuleFrame(step.key))
            end
        end)
    end

    if self.RefreshOptionsPanels then
        self:RefreshOptionsPanels()
    end

    return true, string.format(L["RUNTIME_ONBOARDING_GUIDED_SETUP_STEP_READY_FORMAT"], step.title)
end

function PortalAuthority:StartOnboardingD4Tour(reason)
    local canStart, blockMessage = self:CanStartOnboardingD4()
    if not canStart then
        return false, blockMessage
    end

    local runtime = PA_OnboardingD4GetRuntime(self)
    if runtime.active then
        self:RefreshOnboardingD4Card()
        self:FocusOnboardingD4Card()
        self:BindOnboardingD4HighlightToFrame(self:GetOnboardingD4ActiveModuleFrame(runtime.currentStepKey))
        return true, string.format(L["RUNTIME_ONBOARDING_GUIDED_SETUP_ALREADY_ACTIVE_FORMAT"], tostring(reason or "manual"))
    end

    local staleSession = self.Profiles_GetOnboardingD4Session and self:Profiles_GetOnboardingD4Session() or nil
    if type(staleSession) == "table" then
        local okStale = self:ResolveAbandonedOnboardingD4Session("stale-session")
        if not okStale then
            return false, L["RUNTIME_ONBOARDING_GUIDED_SETUP_RECOVER_PREVIOUS_FAILED"]
        end
        runtime = PA_OnboardingD4GetRuntime(self)
    end

    self:HidePremiumWelcome()
    self:HideSettingsPanelForOnboardingD4()

    runtime.active = true
    runtime.stepIndex = nil
    runtime.currentStepKey = nil
    runtime.movedPositions = {}
    runtime.stepEntryPositions = {}
    runtime.recoverySession = self:CaptureOnboardingD4RecoverySession()
    runtime.previousDeferSharedMarkerTickerRefresh = self._deferSharedMarkerTickerRefresh == true

    self._deferSharedMarkerTickerRefresh = true
    if self.UpdateMoveHintTickerState then
        self:UpdateMoveHintTickerState()
    end

    if self.Profiles_SetOnboardingD4Session then
        self:Profiles_SetOnboardingD4Session(runtime.recoverySession)
    end

    self:EnsureOnboardingD4Card()
    return self:ShowOnboardingD4Step(1)
end

function PortalAuthority:GetOnboardingD4StatusLines()
    local runtime = PA_OnboardingD4GetRuntime(self)
    local session = runtime.recoverySession
    if type(session) ~= "table" and self.Profiles_GetOnboardingD4Session then
        session = self:Profiles_GetOnboardingD4Session()
    end

    if runtime.active then
        self:SyncOnboardingD4ActiveStepPosition()
    end

    local step = self:GetOnboardingD4Step(runtime.currentStepKey or runtime.stepIndex or (session and session.currentStepKey))
    local movedKeys = PA_OnboardingD4GetMovedKeys(runtime)
    local movedLabels = {}
    for _, key in ipairs(movedKeys) do
        local descriptor = self:GetOnboardingD4Step(key)
        movedLabels[#movedLabels + 1] = descriptor and descriptor.title or key
    end

    local lines = {}
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r D3 gate=%s", PA_IsOnboardingD3DevGateEnabled() and "ON" or "OFF")
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r D4 gate=%s", PA_IsOnboardingD4DevGateEnabled() and "ON" or "OFF")
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r D4 active=%s", tostring(runtime.active))
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r currentStep=%s", step and string.format("%d/%d (%s)", step.index, #PA_ONBOARDING_D4_STEPS, step.title) or "nil")
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r recoverySession=%s", type(session) == "table" and "present" or "missing")
    lines[#lines + 1] = string.format("|cffffd100Portal Authority:|r moved=%s", #movedLabels > 0 and table.concat(movedLabels, ", ") or "none")
    return lines
end

function PortalAuthority:HandleOnboardingD4Slash(rawAction)
    local action = trim(tostring(rawAction or "")):lower()
    if action == "" or action == "help" then
        PA_PrintPortalAuthorityMessage("usage: /pa dev onboarding-d4 status|start|stop")
        return true
    end

    if action == "status" then
        for _, line in ipairs(self:GetOnboardingD4StatusLines()) do
            print(line)
        end
        return true
    end

    if action == "start" then
        local ok, response = self:StartOnboardingD4Tour("dev-start")
        PA_PrintPortalAuthorityMessage(response or (ok and L["RUNTIME_ONBOARDING_GUIDED_SETUP_STARTED"] or L["RUNTIME_ONBOARDING_GUIDED_SETUP_FAILED"]))
        return true
    end

    if action == "stop" then
        local ok, response = self:CompleteOnboardingD4Session({
            preserveMovedPositions = false,
            exitReason = "dev-stop",
        })
        PA_PrintPortalAuthorityMessage(response or (ok and L["RUNTIME_ONBOARDING_GUIDED_SETUP_STOPPED"] or L["RUNTIME_ONBOARDING_GUIDED_SETUP_CLEANUP_FAILED"]))
        return true
    end

    PA_PrintPortalAuthorityMessage("usage: /pa dev onboarding-d4 status|start|stop")
    return true
end

function PortalAuthority:_OnboardingEnsureMarkers(timersFrameOrHandle, dockFrame, deathHandle)
    self._onboardingMarkers = self._onboardingMarkers or {}
    self._onboardingMarkerActive = self._onboardingMarkerActive or {}

    local markers = self._onboardingMarkers
    local active = self._onboardingMarkerActive
    local fontPath = STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF"
    local timersModule = self.Modules and self.Modules.registry and self.Modules.registry.timers
    local timersAnchor = timersFrameOrHandle or (timersModule and (timersModule.dragHandle or timersModule.frame or timersModule.mainFrame)) or nil
    local dockAnchor = dockFrame or self.dockFrame or nil
    local deathAnchor = deathHandle or (self.DeathAlerts and self.DeathAlerts.handle) or nil

    local function ensureMarker(key, labelText)
        local fs = markers[key]
        if not fs then
            fs = UIParent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
            fs:SetFont(fontPath, 32, "OUTLINE")
            fs:SetTextColor(1, 0, 0, 1)
            fs:SetJustifyH("CENTER")
            fs:SetJustifyV("MIDDLE")
            markers[key] = fs
        end
        fs:SetText(labelText)
        return fs
    end

    local timersLabel = ensureMarker("timers", tostring(L["RUNTIME_MOVE_HINT_TIMERS_LABEL"] or "Timers"))
    timersLabel:ClearAllPoints()
    if timersAnchor then
        local timersOffsetY = 10
        if timersModule and timersModule.dragHandle and timersAnchor == timersModule.dragHandle then
            timersOffsetY = 6
        end
        timersLabel:SetPoint("BOTTOM", timersAnchor, "TOP", 0, timersOffsetY)
        timersLabel:Show()
        active.timers = true
    else
        timersLabel:Hide()
        active.timers = false
    end

    local dockLabel = ensureMarker("dock", tostring(L["RUNTIME_MOVE_HINT_DOCK_LABEL"] or "M+ Dock"))
    dockLabel:ClearAllPoints()
    if dockAnchor then
        dockLabel:SetPoint("BOTTOM", dockAnchor, "TOP", 0, 54)
        dockLabel:Show()
        active.dock = true
    else
        dockLabel:Hide()
        active.dock = false
    end

    local deathLabel = ensureMarker("death", tostring(L["RUNTIME_MOVE_HINT_COMBAT_LABEL"] or "Combat Alerts"))
    deathLabel:ClearAllPoints()
    if deathAnchor then
        deathLabel:SetPoint("BOTTOM", deathAnchor, "TOP", 0, 6)
        deathLabel:Show()
        active.death = true
    else
        deathLabel:Hide()
        active.death = false
    end
end

function PortalAuthority:_OnboardingUpdateMarkers(blinkOn)
    if not self._onboardingMarkers then
        return
    end

    local markers = self._onboardingMarkers
    local active = self._onboardingMarkerActive or {}
    local db = PortalAuthorityDB

    local function updateMarker(key, shouldShow)
        local fs = markers[key]
        if not fs then return end
        if not shouldShow or active[key] == false then
            fs:Hide()
            return
        end
        fs:SetAlpha(blinkOn and 1 or 0.15)
        fs:Show()
    end

    local function getMoveHintSurfaceUnlockState()
        local dockUnlocked = db and (PA_SafeBool(db.dockEnabled) and not PA_SafeBool(db.dockLocked)) or false
        local timersDb = db and db.modules and db.modules.timers or nil
        local timersEnabled = timersDb and timersDb.enabled ~= false or false
        local timersUnlocked = timersEnabled and timersDb and timersDb.locked == false or false
        local combatAlertsEnabled = (self.IsCombatAlertsEnabled and self:IsCombatAlertsEnabled())
        if combatAlertsEnabled == nil then
            combatAlertsEnabled = db and db.combatAlertsEnabled ~= false or false
        end
        local deathUnlocked = combatAlertsEnabled and db and (not PA_SafeBool(db.deathAlertLocked)) or false
        return dockUnlocked, timersUnlocked, deathUnlocked
    end

    local dockUnlocked, timersUnlocked, deathUnlocked = getMoveHintSurfaceUnlockState()

    updateMarker("dock", dockUnlocked)
    updateMarker("timers", timersUnlocked)
    updateMarker("death", deathUnlocked)
end

function PortalAuthority:ShouldRunMoveHintTicker()
    local db = PortalAuthorityDB

    if self.IsOnboardingD4Active and self:IsOnboardingD4Active() then
        return false
    end

    local dockUnlocked = db and (PA_SafeBool(db.dockEnabled) and not PA_SafeBool(db.dockLocked)) or false
    local timersDb = db and db.modules and db.modules.timers or nil
    local timersEnabled = timersDb and timersDb.enabled ~= false or false
    local timersUnlocked = timersEnabled and timersDb and timersDb.locked == false or false
    local combatAlertsEnabled = (self.IsCombatAlertsEnabled and self:IsCombatAlertsEnabled())
    if combatAlertsEnabled == nil then
        combatAlertsEnabled = db and db.combatAlertsEnabled ~= false or false
    end
    local deathUnlocked = combatAlertsEnabled and db and (not PA_SafeBool(db.deathAlertLocked)) or false
    return dockUnlocked or timersUnlocked or deathUnlocked
end

function PortalAuthority:RefreshMoveHintMarkersImmediate(forceEnsure)
    if self._deferSharedMarkerTickerRefresh then
        return
    end
    if not (self._OnboardingEnsureMarkers and self._OnboardingUpdateMarkers) then
        return
    end

    local shouldEnsure = forceEnsure or self._onboardingMarkers or self:ShouldRunMoveHintTicker()
    if shouldEnsure then
        self:_OnboardingEnsureMarkers()
    end
    if self._onboardingMarkers then
        self:_OnboardingUpdateMarkers(self._moveHintBlink == true)
    end
end

function PortalAuthority:StopMoveHintTicker()
    if self._moveHintTicker and self._moveHintTicker.Cancel then
        self._moveHintTicker:Cancel()
    end
    self._moveHintTicker = nil
end

function PortalAuthority:EnsureMoveHintTicker()
    if PA_IsUiSurfaceGateEnabled() then
        self:StopMoveHintTicker()
        return
    end
    if self._moveHintTicker or not self:ShouldRunMoveHintTicker() then
        return
    end
    if not (C_Timer and C_Timer.NewTicker) then
        return
    end

    if self._moveHintBlink == nil then
        self._moveHintBlink = false
    end
    self:RefreshMoveHintMarkersImmediate(true)
    self._moveHintTicker = C_Timer.NewTicker(0.5, function()
        local perfStart, perfState = self:PerfBegin("movehint_ticker")
        self:CpuDiagCount("movehint_ticker")
        local ok = pcall(function()
            if not self:ShouldRunMoveHintTicker() then
                self:StopMoveHintTicker()
                self._moveHintBlink = false
                if self._onboardingMarkers then
                    self:_OnboardingUpdateMarkers(false)
                end
                return
            end

            self._moveHintBlink = not self._moveHintBlink
            self:RefreshMoveHintMarkersImmediate(true)
        end)
        self:PerfEnd("movehint_ticker", perfStart, perfState)
        if not ok then
            -- Keep ticker alive; retry on next tick.
        end
    end)
end

function PortalAuthority:UpdateMoveHintTickerState()
    if PA_IsUiSurfaceGateEnabled() then
        self:StopMoveHintTicker()
        self._moveHintBlink = false
        if self._onboardingMarkers then
            for _, fs in pairs(self._onboardingMarkers) do
                if fs and fs.Hide then
                    fs:Hide()
                end
            end
        end
        return
    end
    if self._deferSharedMarkerTickerRefresh then
        return
    end
    local shouldRun = self:ShouldRunMoveHintTicker()
    if shouldRun then
        if self._moveHintBlink == nil then
            self._moveHintBlink = false
        end
        self:RefreshMoveHintMarkersImmediate(true)
        self:EnsureMoveHintTicker()
        return
    end

    self:StopMoveHintTicker()
    self._moveHintBlink = false
    if self._onboardingMarkers then
        self:_OnboardingUpdateMarkers(false)
    end
end

CoreInternal.PA_PrintPortalAuthorityMessage = PA_PrintPortalAuthorityMessage
