local addon_name, ns = ...
local MM = ns.MM
local ADDON_NAME = addon_name or "MurlocMinder"

local ICON_EGG = "Interface\\Icons\\INV_Pet_PinkMurlocEgg"
local ICON_MURLOC = "Interface\\Icons\\INV_Pet_BabyMurlocs_Blue"
local EGG_DISPLAY_ID = 47755
local ADOLESCENT_DISPLAY_ID = 617
local EGG_MODEL_BASE_FACING = math.rad(-8)
local EGG_MODEL_BASE_X = 0
local EGG_MODEL_BASE_Y = 0
local EGG_MODEL_BASE_Z = 0
local EGG_MODEL_BASE_SCALE = 1.05
local EGG_MODEL_BASE_CAM = 1.30
local ADOLESCENT_MODEL_BASE_FACING = math.rad(-14)
local ADOLESCENT_MODEL_BASE_X = 0
local ADOLESCENT_MODEL_BASE_Y = 0
local ADOLESCENT_MODEL_BASE_Z = -0.115
local ADOLESCENT_MODEL_BASE_SCALE = 0.86
local ADOLESCENT_MODEL_BASE_CAM = 3.10
local MODEL_POS_X_MIN = -8.00
local MODEL_POS_X_MAX = 8.00
local MODEL_POS_Y_MIN = -4.00
local MODEL_POS_Y_MAX = 4.00
local MODEL_POS_Z_MIN = -4.00
local MODEL_POS_Z_MAX = 4.00
local MODEL_POS_Y_EDGE_BASE = 1.90
local MODEL_POS_Z_EDGE_BASE = 1.85
local MODEL_EDGE_SCALE_MIN_FACTOR = 0.45
local MODEL_EDGE_SCALE_MAX_FACTOR = 2.60
local MODEL_SCALE_MIN = 0.31
local MODEL_SCALE_MAX = 1.85
local MODEL_MOVE_PER_PIXEL = 0.0023
local MODEL_SPIN_PER_PIXEL = 0.0105
local MODEL_DEPTH_STEP = 0.16
local MODEL_CONTROL_SCHEMA_VERSION = 5
local MODEL_UNCLAMP_DRAG = true
local MODEL_DRAG_COEFF_MIN = 0.35
local MODEL_DRAG_COEFF_MAX = 8.00
local MODEL_DRAG_NEAR_REDUCTION = 0.60
local MODEL_DRAG_FAR_GAIN = 5.50
local MODEL_DRAG_DISTANCE_GAIN = 3.00
local MODEL_CONTROLS_ENABLED = false
local MODEL_STAGE_DEFAULTS = {
    egg = {
        x = EGG_MODEL_BASE_X,
        y = EGG_MODEL_BASE_Y,
        z = EGG_MODEL_BASE_Z,
        facing = EGG_MODEL_BASE_FACING,
        scale = EGG_MODEL_BASE_SCALE,
        cam = EGG_MODEL_BASE_CAM,
    },
    adolescent = {
        x = ADOLESCENT_MODEL_BASE_X,
        y = ADOLESCENT_MODEL_BASE_Y,
        z = ADOLESCENT_MODEL_BASE_Z,
        facing = ADOLESCENT_MODEL_BASE_FACING,
        scale = ADOLESCENT_MODEL_BASE_SCALE,
        cam = ADOLESCENT_MODEL_BASE_CAM,
    },
}
local LEFT_PANEL_WIDTH = 320
local RIGHT_PANEL_WIDTH = 768
local ACTIVITY_SCROLL_STEP = 20
local BASIC_ACTION_UI_VALUE = 5
local DEV_MURLOC_SOUND_ROW_HEIGHT = 19
local DEV_MURLOC_SOUND_VISIBLE_ROWS = 5
local DEV_MURLOC_SOUND_LIST_HEIGHT = 106

local COLOR_WHITE = { 1.0, 1.0, 1.0 }
local COLOR_HUNGER = { 0.38, 0.92, 0.57 }      -- green
local COLOR_JOY = { 0.35, 0.72, 1.0 }          -- blue
local COLOR_ENERGY = { 0.97, 0.8, 0.31 }       -- amber
local COLOR_CLEAN = { 0.76, 0.58, 0.95 }       -- light purple
local COLOR_HATCH = { 0.96, 0.42, 0.72 }       -- rose (distinct from need bars)
local COLOR_SHELL = { 0.82, 0.84, 0.88 }       -- light grey (silver-like)
local COLOR_NEGATIVE = { 1.0, 0.25, 0.25 }     -- red
local TITLE_VERSION_FALLBACK = "0.1.10"
local TUTORIAL_MASK_ALPHA = 0.76
local TUTORIAL_TARGET_PADDING = 7
local TUTORIAL_CARD_HEIGHT = 178
local TUTORIAL_CARD_HEIGHT_EXPANDED = TUTORIAL_CARD_HEIGHT * 2
local NEED_DELTA_ORDER = { "hunger", "joy", "energy", "cleanliness" }
local TRAIT_TOOLTIP_TRIGGER_ORDER = { "feed", "play", "rest", "clean", "shell_milestone" }
local TRAIT_TOOLTIP_TRIGGER_LABELS = {
    feed = "Feed",
    play = "Play",
    rest = "Rest",
    clean = "Clean",
    shell_milestone = "Shells +100",
}
local TOOLTIP_CONSUMABLE_STORE_KEYS = {
    nameTag = true,
    traitGem = true,
    traitLamp = true,
}

local function GetDisplayVersionText()
    local function parseVersion(text)
        local major, minor, patch, suffix = tostring(text or ""):match("^(%d+)%.(%d+)%.(%d+)([A-Za-z]?)$")
        if major then
            return major, minor, patch, suffix or ""
        end
        return nil
    end

    if MM and type(MM.GetAddonVersionText) == "function" then
        local resolved = tostring(MM:GetAddonVersionText() or "")
        local major, minor, patch, suffix = parseVersion(resolved)
        if major then
            return string.format("v%s.%s.%s%s", major, minor, patch, suffix)
        end
    end

    local version = ""
    if GetAddOnMetadata then
        version = tostring(GetAddOnMetadata(ADDON_NAME, "Version") or "")
        if version == "" then
            version = tostring(GetAddOnMetadata("MurlocMinder", "Version") or "")
        end
    end

    local major, minor, patch, suffix = parseVersion(version)
    if not major then
        major, minor, patch, suffix = parseVersion(TITLE_VERSION_FALLBACK)
    end
    if major then
        return string.format("v%s.%s.%s%s", major, minor, patch, suffix or "")
    end
    return "v0.1.10"
end

local function ClampNumber(value, minValue, maxValue)
    local n = tonumber(value) or minValue
    if n < minValue then
        return minValue
    end
    if n > maxValue then
        return maxValue
    end
    return n
end

local function GetFrameRectUnion(frames)
    if type(frames) ~= "table" then
        return nil
    end

    local left, bottom, right, top
    for _, target in ipairs(frames) do
        if target and target.GetRect and target:IsShown() then
            local x, y, w, h = target:GetRect()
            if x and y and w and h and w > 0 and h > 0 then
                local r = x + w
                local t = y + h
                left = left and math.min(left, x) or x
                bottom = bottom and math.min(bottom, y) or y
                right = right and math.max(right, r) or r
                top = top and math.max(top, t) or t
            end
        end
    end

    if not left then
        return nil
    end
    return left, bottom, right, top
end

local function ColorizeNeedDeltaText(valueText, color)
    local r = math.floor((color[1] or 1) * 255 + 0.5)
    local g = math.floor((color[2] or 1) * 255 + 0.5)
    local b = math.floor((color[3] or 1) * 255 + 0.5)
    return string.format("|cff%02x%02x%02x%s|r", r, g, b, tostring(valueText or ""))
end

local function FormatNeedDelta(value, needKey)
    local color = COLOR_WHITE
    if needKey == "hunger" then
        color = COLOR_HUNGER
    elseif needKey == "joy" then
        color = COLOR_JOY
    elseif needKey == "energy" then
        color = COLOR_ENERGY
    elseif needKey == "cleanliness" then
        color = COLOR_CLEAN
    end
    local amount = math.floor((tonumber(value) or 0) + 0.5)
    if amount >= 0 then
        return ColorizeNeedDeltaText(string.format("+%d%%", amount), color)
    end
    return ColorizeNeedDeltaText(string.format("%d%%", amount), color)
end

local function FormatNeedEffectsInDescription(desc)
    local out = tostring(desc or "")

    local function repl(sign, amount, needWord)
        local need = string.lower(tostring(needWord or ""))
        local n = tonumber(amount) or 0
        if sign == "-" then
            n = -n
        end
        if need == "hunger" or need == "joy" or need == "energy" or need == "cleanliness" then
            return FormatNeedDelta(n, need)
        end
        return string.format("%s%s%%", sign or "+", tostring(amount or "0"))
    end

    out = out:gsub("([%+%-])(%d+)%%?%s*[Hh]unger", repl)
    out = out:gsub("([%+%-])(%d+)%%?%s*[Jj]oy", repl)
    out = out:gsub("([%+%-])(%d+)%%?%s*[Ee]nergy", repl)
    out = out:gsub("([%+%-])(%d+)%%?%s*[Cc]leanliness", repl)

    return out
end
local DEV_BACKGROUND_CHOICES = {
    { key = "auto", label = "Auto (Default)" },
    { key = "murloc_shop", label = "Murloc Habitat", atlas = "shop-bg-environment-unavailable-murloc", textures = { "Interface\\Shop\\CatalogShopUnavailableMurloc" } },
    { key = "scene_plunderstorm", label = "Plunderstorm Harbor (3D)", modelFileID = 5582785, previewAtlas = "plunderstorm-toast-BG-matchComplete" },
    { key = "twin_peaks", label = "Twin Peaks", textures = { "Interface\\PVPFrame\\PvpBg-TwinPeaks-ToastBG" } },
    { key = "elwynn_housing", label = "Elwynn", atlas = "housing-dashboard-bg-elwynn" },
    { key = "exiles_reach", label = "Exile's Reach", atlas = "charactercreate-startingzone-exilesreach" },
    { key = "queens_conservatory", label = "Queen's Conservatory", atlas = "queensconservatory-bg" },
}
local DEV_BACKGROUND_CHOICES_BY_KEY = {}
for _, entry in ipairs(DEV_BACKGROUND_CHOICES) do
    DEV_BACKGROUND_CHOICES_BY_KEY[entry.key] = entry
end

local function GetDevBackgroundLabel(choiceKey)
    local entry = DEV_BACKGROUND_CHOICES_BY_KEY[choiceKey]
    if entry then
        return entry.label
    end
    return "Auto (Default)"
end

-- Fallback murloc file IDs (sourced from the local client sound index) so Dev can list clips
-- even when SOUNDKIT has no murloc-named constants.
local DEV_MURLOC_SOUND_FILE_FALLBACK = {
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_attack_5305059.ogg", id = 5305059 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_attack_5305061.ogg", id = 5305061 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_attack_5305063.ogg", id = 5305063 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_attack_5305065.ogg", id = 5305065 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_death_5305091.ogg", id = 5305091 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_fidget_5305075.ogg", id = 5305075 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_fidget_5305077.ogg", id = 5305077 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_fidget_5305081.ogg", id = 5305081 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_fidget_5305085.ogg", id = 5305085 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_greetings_5305067.ogg", id = 5305067 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_greetings_5305069.ogg", id = 5305069 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_greetings_5305071.ogg", id = 5305071 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_greetings_5305073.ogg", id = 5305073 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_greetings_5305079.ogg", id = 5305079 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_greetings_5305083.ogg", id = 5305083 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_greetings_5305087.ogg", id = 5305087 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_greetings_5305089.ogg", id = 5305089 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_wound_5305093.ogg", id = 5305093 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_wound_5305095.ogg", id = 5305095 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_wound_5305097.ogg", id = 5305097 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_wound_5305099.ogg", id = 5305099 },
  { path = "sound/creature/azmerlothmurlocpet/azmerlothmurlocpet_wound_5305101.ogg", id = 5305101 },
  { path = "sound/creature/babymurloc/babymurloc_attack_01.ogg", id = 657574 },
  { path = "sound/creature/babymurloc/babymurloc_attack_02.ogg", id = 657576 },
  { path = "sound/creature/babymurloc/babymurloc_attack_03.ogg", id = 657578 },
  { path = "sound/creature/babymurloc/babymurloc_attack_04.ogg", id = 657580 },
  { path = "sound/creature/babymurloc/babymurloc_attack_05.ogg", id = 657582 },
  { path = "sound/creature/babymurloc/babymurloc_attack_06.ogg", id = 657584 },
  { path = "sound/creature/babymurloc/babymurloc_clickable_04.ogg", id = 657586 },
  { path = "sound/creature/babymurloc/babymurloc_clickable_05.ogg", id = 657588 },
  { path = "sound/creature/babymurloc/babymurloc_clickable_06.ogg", id = 657590 },
  { path = "sound/creature/babymurloc/babymurloc_clickable_07.ogg", id = 657592 },
  { path = "sound/creature/babymurloc/babymurloc_clickable_08.ogg", id = 657594 },
  { path = "sound/creature/babymurloc/babymurloc_clickable_09.ogg", id = 657596 },
  { path = "sound/creature/babymurloc/babymurloc_clickable_10.ogg", id = 657598 },
  { path = "sound/creature/babymurloc/babymurloc_death_01.ogg", id = 657600 },
  { path = "sound/creature/babymurloc/babymurloc_death_02.ogg", id = 657602 },
  { path = "sound/creature/babymurloc/babymurloc_death_03.ogg", id = 657604 },
  { path = "sound/creature/babymurloc/babymurloc_wound_01.ogg", id = 657606 },
  { path = "sound/creature/babymurloc/babymurloc_wound_02.ogg", id = 657608 },
  { path = "sound/creature/babymurloc/babymurloc_wound_03.ogg", id = 657610 },
  { path = "sound/creature/babymurloc/babymurloc_wound_04.ogg", id = 657612 },
  { path = "sound/creature/babymurloc/babymurloc_wound_05.ogg", id = 657614 },
  { path = "sound/creature/babymurloc/babymurloca.ogg", id = 544712 },
  { path = "sound/creature/babymurloc/babymurlocb.ogg", id = 544713 },
  { path = "sound/creature/babymurloc/babymurlocc.ogg", id = 544715 },
  { path = "sound/creature/babymurloc/babymurlocdance.ogg", id = 544714 },
  { path = "sound/creature/babymurloc/babymurlocdance_02.ogg", id = 657572 },
  { path = "sound/creature/babymurlocice/babymurlocice_fidget_6182348.ogg", id = 6182348 },
  { path = "sound/creature/babymurlocice/babymurlocice_fidget_6182350.ogg", id = 6182350 },
  { path = "sound/creature/babymurlocice/babymurlocice_fidget_6182352.ogg", id = 6182352 },
  { path = "sound/creature/babymurlocice/babymurlocice_fidget_6182354.ogg", id = 6182354 },
  { path = "sound/creature/babymurlocice/babymurlocice_fidget_6182356.ogg", id = 6182356 },
  { path = "sound/creature/babymurlocice/babymurlocice_fidget_6182358.ogg", id = 6182358 },
  { path = "sound/creature/babymurlocice/babymurlocice_fidget_6182366.ogg", id = 6182366 },
  { path = "sound/creature/babymurlocice/babymurlocice_fidget_6182370.ogg", id = 6182370 },
  { path = "sound/creature/babymurlocice/babymurlocice_fidget_6182372.ogg", id = 6182372 },
  { path = "sound/creature/babymurlocice/babymurlocice_fidget_6182374.ogg", id = 6182374 },
  { path = "sound/creature/babymurlocice/babymurlocice_fidget_6182376.ogg", id = 6182376 },
  { path = "sound/creature/babymurlocice/babymurlocice_fidget_6182378.ogg", id = 6182378 },
  { path = "sound/creature/babymurlocice/babymurlocice_greetings_6182360.ogg", id = 6182360 },
  { path = "sound/creature/babymurlocice/babymurlocice_greetings_6182362.ogg", id = 6182362 },
  { path = "sound/creature/babymurlocice/babymurlocice_greetings_6182364.ogg", id = 6182364 },
  { path = "sound/creature/babymurlocice/babymurlocice_greetings_6182368.ogg", id = 6182368 },
  { path = "sound/creature/babymurlocice/babymurlocice_wound_6182386.ogg", id = 6182386 },
  { path = "sound/creature/babymurlocice/babymurlocice_wound_6182388.ogg", id = 6182388 },
  { path = "sound/creature/babymurlocice/babymurlocice_wound_6182390.ogg", id = 6182390 },
  { path = "sound/creature/babymurlocice/babymurlocice_wound_6182392.ogg", id = 6182392 },
  { path = "sound/creature/babymurlocice/babymurlocice_wound_6182394.ogg", id = 6182394 },
  { path = "sound/creature/deathwingbabymurloc/deathwingbabymurloc_attackcritical_3612013.ogg", id = 3612013 },
  { path = "sound/creature/deathwingbabymurloc/deathwingbabymurloc_attackcritical_3612015.ogg", id = 3612015 },
  { path = "sound/creature/deathwingbabymurloc/deathwingbabymurloc_attackcritical_3612017.ogg", id = 3612017 },
  { path = "sound/creature/deathwingbabymurloc/deathwingbabymurloc_attackcritical_3612019.ogg", id = 3612019 },
  { path = "sound/creature/deathwingbabymurloc/deathwingbabymurloc_attackcritical_3612021.ogg", id = 3612021 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_attack.ogg", id = 554969 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_attack_01.ogg", id = 657664 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_attack_02.ogg", id = 657666 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_attack_03.ogg", id = 657668 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_attack_04.ogg", id = 657670 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_attack_05.ogg", id = 657672 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_attack_06.ogg", id = 657674 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_click_01.ogg", id = 554978 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_click_02.ogg", id = 554964 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_click_03.ogg", id = 554967 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_click_04.ogg", id = 554975 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_click_05.ogg", id = 554970 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_click_06.ogg", id = 554965 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_click_07.ogg", id = 554977 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_click_08.ogg", id = 657676 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_click_09.ogg", id = 657678 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_click_10.ogg", id = 657680 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_death.ogg", id = 554966 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_stand1_01.ogg", id = 554968 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_stand1_02.ogg", id = 554976 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_stand1_03.ogg", id = 554973 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_stand2_01.ogg", id = 554971 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_stand2_02.ogg", id = 554974 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_stand2_03.ogg", id = 554972 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_wound_01.ogg", id = 657682 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_wound_02.ogg", id = 657684 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_wound_03.ogg", id = 657686 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_wound_04.ogg", id = 657688 },
  { path = "sound/creature/marinebabymurloc/marinebabymurloc_wound_05.ogg", id = 657690 },
  { path = "sound/creature/mr_mrglglrm/vo_703_mr_mrglglrm_01.ogg", id = 1404466 },
  { path = "sound/creature/mr_mrglglrm/vo_703_mr_mrglglrm_02.ogg", id = 1404467 },
  { path = "sound/creature/mr_mrglglrm/vo_703_mr_mrglglrm_03.ogg", id = 1404468 },
  { path = "sound/creature/mr_mrglglrm/vo_703_mr_mrglglrm_04.ogg", id = 1404469 },
  { path = "sound/creature/mr_mrglglrm/vo_703_mr_mrglglrm_05.ogg", id = 1404470 },
  { path = "sound/creature/mr_mrglglrm/vo_703_mr_mrglglrm_06.ogg", id = 1404471 },
  { path = "sound/creature/mr_mrglglrm/vo_703_mr_mrglglrm_07.ogg", id = 1404472 },
  { path = "sound/creature/murloc/mmurlocaggroa.ogg", id = 555997 },
  { path = "sound/creature/murloc/mmurlocaggrob.ogg", id = 555992 },
  { path = "sound/creature/murloc/mmurlocaggroc.ogg", id = 556006 },
  { path = "sound/creature/murloc/mmurlocaggroold.ogg", id = 556000 },
  { path = "sound/creature/murloc/mmurlocattacka.ogg", id = 556003 },
  { path = "sound/creature/murloc/mmurlocattackb.ogg", id = 556004 },
  { path = "sound/creature/murloc/mmurlocattackc.ogg", id = 556005 },
  { path = "sound/creature/murloc/mmurlocdeath2a.ogg", id = 555995 },
  { path = "sound/creature/murloc/mmurlocfidget2a.ogg", id = 555993 },
  { path = "sound/creature/murloc/mmurlocfidget2b.ogg", id = 556002 },
  { path = "sound/creature/murloc/mmurlocfidget3a.ogg", id = 556001 },
  { path = "sound/creature/murloc/mmurlocpreaggroa.ogg", id = 555998 },
  { path = "sound/creature/murloc/mmurlocwounda.ogg", id = 555999 },
  { path = "sound/creature/murloc/mmurlocwoundb.ogg", id = 555996 },
  { path = "sound/creature/murloc/mmurlocwoundc.ogg", id = 555994 },
  { path = "sound/creature/murloc/mmurlocwoundcriticala.ogg", id = 556007 },
  { path = "sound/creature/murloccrusader/mon_pet_murloccrusader_standv2_01.ogg", id = 929253 },
  { path = "sound/creature/murloccrusader/mon_pet_murloccrusader_standv2_02.ogg", id = 929255 },
  { path = "sound/creature/murloccrusader/mon_pet_murloccrusader_standv2_03.ogg", id = 929257 },
  { path = "sound/creature/murloccrusader/mon_pet_murloccrusader_standv3_01.ogg", id = 929259 },
  { path = "sound/creature/murloccrusader/mon_pet_murloccrusader_standv3_02.ogg", id = 929261 },
  { path = "sound/creature/murloccrusader/mon_pet_murloccrusader_standv3_03.ogg", id = 929263 },
  { path = "sound/creature/murloccrusader/mon_pet_murloccrusader_standv4_01.ogg", id = 929265 },
  { path = "sound/creature/murloccrusader/mon_pet_murloccrusader_standv4_02.ogg", id = 929267 },
  { path = "sound/creature/murloccrusader/mon_pet_murloccrusader_standv4_03.ogg", id = 929269 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_aggro_01.ogg", id = 556014 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_aggro_02.ogg", id = 556009 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_aggro_03.ogg", id = 556032 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_aggro_04.ogg", id = 556026 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_aggro_05.ogg", id = 556063 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_aggro_06.ogg", id = 556028 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_aggro_07.ogg", id = 556008 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_aggro_08.ogg", id = 556043 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_aggro_09.ogg", id = 556019 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attack_01.ogg", id = 556052 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attack_02.ogg", id = 556030 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attack_03.ogg", id = 556034 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attack_04.ogg", id = 556038 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attack_05.ogg", id = 556035 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attack_06.ogg", id = 556020 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attack_07.ogg", id = 556022 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attack_08.ogg", id = 556059 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attack_09.ogg", id = 556056 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attack_10.ogg", id = 556018 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attackcrit_01.ogg", id = 556015 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attackcrit_02.ogg", id = 556054 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attackcrit_03.ogg", id = 556012 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attackcrit_04.ogg", id = 556039 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attackcrit_05.ogg", id = 556029 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_attackcrit_06.ogg", id = 556031 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_death_01.ogg", id = 556048 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_death_02.ogg", id = 556024 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_death_03.ogg", id = 556045 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_death_04.ogg", id = 556064 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_death_05.ogg", id = 556051 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_death_06.ogg", id = 556065 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_death_07.ogg", id = 556061 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_death_08.ogg", id = 556013 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_death_09.ogg", id = 556025 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_death_10.ogg", id = 556033 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_preaggro_01.ogg", id = 556021 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_preaggro_02.ogg", id = 556041 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_preaggro_03.ogg", id = 556066 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_preaggro_04.ogg", id = 556037 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_preaggro_05.ogg", id = 556055 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_preaggro_06.ogg", id = 556027 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_preaggro_07.ogg", id = 556042 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_preaggro_08.ogg", id = 556060 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_preaggro_09.ogg", id = 556044 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_wound_01.ogg", id = 556046 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_wound_02.ogg", id = 556036 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_wound_03.ogg", id = 556040 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_wound_04.ogg", id = 556023 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_wound_05.ogg", id = 556049 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_wound_06.ogg", id = 556058 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_wound_07.ogg", id = 556053 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_wound_08.ogg", id = 556010 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_woundcrit_01.ogg", id = 556011 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_woundcrit_02.ogg", id = 556050 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_woundcrit_03.ogg", id = 556057 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_woundcrit_04.ogg", id = 556016 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_woundcrit_05.ogg", id = 556047 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_woundcrit_06.ogg", id = 556017 },
  { path = "sound/creature/murlocdeepsea/murlocdeepsea_woundcrit_07.ogg", id = 556062 },
  { path = "sound/creature/murlocmount/murlocmount_aggro_3612003.ogg", id = 3612003 },
  { path = "sound/creature/murlocmount/murlocmount_aggro_3612005.ogg", id = 3612005 },
  { path = "sound/creature/murlocmount/murlocmount_aggro_3612007.ogg", id = 3612007 },
  { path = "sound/creature/murlocmount/murlocmount_aggro_3612009.ogg", id = 3612009 },
  { path = "sound/creature/murlocmount/murlocmount_aggro_3612011.ogg", id = 3612011 },
  { path = "sound/creature/murlocmount/murlocmount_alert_3612103.ogg", id = 3612103 },
  { path = "sound/creature/murlocmount/murlocmount_alert_3612105.ogg", id = 3612105 },
  { path = "sound/creature/murlocmount/murlocmount_alert_3612107.ogg", id = 3612107 },
  { path = "sound/creature/murlocmount/murlocmount_alert_3612109.ogg", id = 3612109 },
  { path = "sound/creature/murlocmount/murlocmount_battleshout_3612043.ogg", id = 3612043 },
  { path = "sound/creature/murlocmount/murlocmount_battleshout_3612045.ogg", id = 3612045 },
  { path = "sound/creature/murlocmount/murlocmount_battleshout_3612047.ogg", id = 3612047 },
  { path = "sound/creature/murlocmount/murlocmount_battleshout_3612049.ogg", id = 3612049 },
  { path = "sound/creature/murlocmount/murlocmount_death_3612051.ogg", id = 3612051 },
  { path = "sound/creature/murlocmount/murlocmount_death_3612053.ogg", id = 3612053 },
  { path = "sound/creature/murlocmount/murlocmount_death_3612055.ogg", id = 3612055 },
  { path = "sound/creature/murlocmount/murlocmount_death_3612057.ogg", id = 3612057 },
  { path = "sound/creature/murlocmount/murlocmount_death_3612059.ogg", id = 3612059 },
  { path = "sound/creature/murlocmount/murlocmount_fidget12_4674497.ogg", id = 4674497 },
  { path = "sound/creature/murlocmount/murlocmount_fidget12_4674499.ogg", id = 4674499 },
  { path = "sound/creature/murlocmount/murlocmount_fidget12_4674501.ogg", id = 4674501 },
  { path = "sound/creature/murlocmount/murlocmount_fidget12_4674503.ogg", id = 4674503 },
  { path = "sound/creature/murlocmount/murlocmount_fidget12_4674505.ogg", id = 4674505 },
  { path = "sound/creature/murlocmount/murlocmount_fidget12_4674507.ogg", id = 4674507 },
  { path = "sound/creature/murlocmount/murlocmount_fidget12_4674509.ogg", id = 4674509 },
  { path = "sound/creature/murlocmount/murlocmount_fidget12_4674511.ogg", id = 4674511 },
  { path = "sound/creature/murlocmount/murlocmount_fidget13_4674485.ogg", id = 4674485 },
  { path = "sound/creature/murlocmount/murlocmount_fidget13_4674487.ogg", id = 4674487 },
  { path = "sound/creature/murlocmount/murlocmount_fidget13_4674489.ogg", id = 4674489 },
  { path = "sound/creature/murlocmount/murlocmount_fidget13_4674491.ogg", id = 4674491 },
  { path = "sound/creature/murlocmount/murlocmount_fidget13_4674493.ogg", id = 4674493 },
  { path = "sound/creature/murlocmount/murlocmount_fidget13_4674495.ogg", id = 4674495 },
  { path = "sound/creature/murlocmount/murlocmount_fidget15_4553803.ogg", id = 4553803 },
  { path = "sound/creature/murlocmount/murlocmount_fidget15_4553805.ogg", id = 4553805 },
  { path = "sound/creature/murlocmount/murlocmount_fidget15_4553807.ogg", id = 4553807 },
  { path = "sound/creature/murlocmount/murlocmount_fidget15_4553809.ogg", id = 4553809 },
  { path = "sound/creature/murlocmount/murlocmount_fidget15_4553811.ogg", id = 4553811 },
  { path = "sound/creature/murlocmount/murlocmount_fidget15_4553813.ogg", id = 4553813 },
  { path = "sound/creature/murlocmount/murlocmount_fidget6_3612023.ogg", id = 3612023 },
  { path = "sound/creature/murlocmount/murlocmount_fidget6_3612025.ogg", id = 3612025 },
  { path = "sound/creature/murlocmount/murlocmount_fidget6_3612027.ogg", id = 3612027 },
  { path = "sound/creature/murlocmount/murlocmount_fidget6_3612029.ogg", id = 3612029 },
  { path = "sound/creature/murlocmount/murlocmount_fidget6_3612031.ogg", id = 3612031 },
  { path = "sound/creature/murlocmount/murlocmount_fidget6_3612033.ogg", id = 3612033 },
  { path = "sound/creature/murlocmount/murlocmount_fidget6_3612035.ogg", id = 3612035 },
  { path = "sound/creature/murlocmount/murlocmount_fidget6_3612037.ogg", id = 3612037 },
  { path = "sound/creature/murlocmount/murlocmount_fidget6_3612039.ogg", id = 3612039 },
  { path = "sound/creature/murlocmount/murlocmount_fidget6_3612041.ogg", id = 3612041 },
  { path = "sound/creature/murlocmount/murlocmount_fidget7_4673995.ogg", id = 4673995 },
  { path = "sound/creature/murlocmount/murlocmount_fidget7_4673997.ogg", id = 4673997 },
  { path = "sound/creature/murlocmount/murlocmount_fidget7_4673999.ogg", id = 4673999 },
  { path = "sound/creature/murlocmount/murlocmount_fidget7_4674001.ogg", id = 4674001 },
  { path = "sound/creature/murlocmount/murlocmount_fidget7_4674003.ogg", id = 4674003 },
  { path = "sound/creature/murlocmount/murlocmount_fidget7_4674005.ogg", id = 4674005 },
  { path = "sound/creature/murlocmount/murlocmount_fidget7_4674007.ogg", id = 4674007 },
  { path = "sound/creature/murlocmount/murlocmount_fidget7_4674009.ogg", id = 4674009 },
  { path = "sound/creature/murlocmount/murlocmount_fidget8_3612085.ogg", id = 3612085 },
  { path = "sound/creature/murlocmount/murlocmount_fidget8_4674196.ogg", id = 4674196 },
  { path = "sound/creature/murlocmount/murlocmount_fidget8_4674198.ogg", id = 4674198 },
  { path = "sound/creature/murlocmount/murlocmount_fidget8_4674200.ogg", id = 4674200 },
  { path = "sound/creature/murlocmount/murlocmount_fidget8_4674204.ogg", id = 4674204 },
  { path = "sound/creature/murlocmount/murlocmount_fidget8_4674206.ogg", id = 4674206 },
  { path = "sound/creature/murlocmount/murlocmount_fidget8_4674210.ogg", id = 4674210 },
  { path = "sound/creature/murlocmount/murlocmount_fidget8_4674214.ogg", id = 4674214 },
  { path = "sound/creature/murlocmount/murlocmount_fidget9_4674212.ogg", id = 4674212 },
  { path = "sound/creature/murlocmount/murlocmount_fidget9_4674477.ogg", id = 4674477 },
  { path = "sound/creature/murlocmount/murlocmount_fidget9_4674479.ogg", id = 4674479 },
  { path = "sound/creature/murlocmount/murlocmount_fidget9_4674481.ogg", id = 4674481 },
  { path = "sound/creature/murlocmount/murlocmount_fidget9_4674483.ogg", id = 4674483 },
  { path = "sound/creature/murlocmount/murlocmount_fidget_3614623.ogg", id = 3614623 },
  { path = "sound/creature/murlocmount/murlocmount_fidget_3614625.ogg", id = 3614625 },
  { path = "sound/creature/murlocmount/murlocmount_fidget_3614627.ogg", id = 3614627 },
  { path = "sound/creature/murlocmount/murlocmount_fidget_3732252.ogg", id = 3732252 },
  { path = "sound/creature/murlocmount/murlocmount_fidget_3732254.ogg", id = 3732254 },
  { path = "sound/creature/murlocmount/murlocmount_fidget_3732256.ogg", id = 3732256 },
  { path = "sound/creature/murlocmount/murlocmount_fidget_3732258.ogg", id = 3732258 },
  { path = "sound/creature/murlocmount/murlocmount_fidget_3732260.ogg", id = 3732260 },
  { path = "sound/creature/murlocmount/murlocmount_fidget_3732262.ogg", id = 3732262 },
  { path = "sound/creature/murlocmount/murlocmount_fidget_3732264.ogg", id = 3732264 },
  { path = "sound/creature/murlocmount/murlocmount_fidget_3732266.ogg", id = 3732266 },
  { path = "sound/creature/murlocmount/murlocmount_fidget_3732268.ogg", id = 3732268 },
  { path = "sound/creature/murlocmount/murlocmount_fidget_3732270.ogg", id = 3732270 },
  { path = "sound/creature/murlocmount/murlocmount_fidget_3732272.ogg", id = 3732272 },
  { path = "sound/creature/murlocmount/murlocmount_fidget_3732274.ogg", id = 3732274 },
  { path = "sound/creature/murlocmount/murlocmount_precast_loop_3610637.ogg", id = 3610637 },
  { path = "sound/creature/murlocmount/murlocmount_precast_loop_3610639.ogg", id = 3610639 },
  { path = "sound/creature/murlocmount/murlocmount_precast_loop_3610641.ogg", id = 3610641 },
  { path = "sound/creature/murlocmount/murlocmount_precast_loop_3612095.ogg", id = 3612095 },
  { path = "sound/creature/murlocmount/murlocmount_precast_loop_3612097.ogg", id = 3612097 },
  { path = "sound/creature/murlocmount/murlocmount_precast_loop_3612099.ogg", id = 3612099 },
  { path = "sound/creature/murlocmount/murlocmount_precast_loop_3612101.ogg", id = 3612101 },
  { path = "sound/creature/murlocmount/murlocmount_wound_3612123.ogg", id = 3612123 },
  { path = "sound/creature/murlocmount/murlocmount_wound_3612125.ogg", id = 3612125 },
  { path = "sound/creature/murlocmount/murlocmount_wound_3612127.ogg", id = 3612127 },
  { path = "sound/creature/murlocmount/murlocmount_wound_3612129.ogg", id = 3612129 },
  { path = "sound/creature/murlocmount/murlocmount_wound_3612131.ogg", id = 3612131 },
  { path = "sound/creature/murlocmount/murlocmount_wound_3612133.ogg", id = 3612133 },
  { path = "sound/creature/murlocmount/murlocmount_wound_3612135.ogg", id = 3612135 },
  { path = "sound/creature/murlocmount/murlocmount_wound_3612137.ogg", id = 3612137 },
  { path = "sound/creature/murlocmount/murlocmount_woundcritical_3612111.ogg", id = 3612111 },
  { path = "sound/creature/murlocmount/murlocmount_woundcritical_3612113.ogg", id = 3612113 },
  { path = "sound/creature/murlocmount/murlocmount_woundcritical_3612115.ogg", id = 3612115 },
  { path = "sound/creature/murlocmount/murlocmount_woundcritical_3612117.ogg", id = 3612117 },
  { path = "sound/creature/murlocmount/murlocmount_woundcritical_3612119.ogg", id = 3612119 },
  { path = "sound/creature/murlocmount/murlocmount_woundcritical_3612121.ogg", id = 3612121 },
}

local DEV_MURLOC_SOUND_LIST_CACHE = nil

local function GetDevMurlocSoundEntries()
    if DEV_MURLOC_SOUND_LIST_CACHE then
        return DEV_MURLOC_SOUND_LIST_CACHE
    end

    local list = {}
    local seen = {}
    local function addEntry(mode, key, id, display)
        if type(key) ~= "string" or type(id) ~= "number" then
            return
        end
        local dedupe = string.format("%s:%d:%s", tostring(mode or "file"), id, key)
        if seen[dedupe] then
            return
        end
        seen[dedupe] = true
        list[#list + 1] = {
            mode = mode or "file",
            key = key,
            id = id,
            display = display or key,
        }
    end

    for _, fallback in ipairs(DEV_MURLOC_SOUND_FILE_FALLBACK or {}) do
        local path = tostring(fallback.path or "")
        local id = tonumber(fallback.id)
        if path ~= "" and id then
            addEntry("file", path, id, path)
        end
    end

    for index, entry in ipairs(list) do
        entry.index = index
    end

    DEV_MURLOC_SOUND_LIST_CACHE = list
    return DEV_MURLOC_SOUND_LIST_CACHE
end

function MM:GetDevMurlocSoundEntries()
    return GetDevMurlocSoundEntries()
end

local function IsConfirmUseStoreEntryKey(entryKey)
    return entryKey == "traitGem" or entryKey == "traitLamp" or entryKey == "nameTag"
end

local function ApplyHabitatBackgroundByChoice(habitatBG, habitatModelBG, habitatShade, choiceKey)
    if not habitatBG then
        return
    end

    local entry = DEV_BACKGROUND_CHOICES_BY_KEY[choiceKey]
    if not entry or choiceKey == "auto" then
        entry = DEV_BACKGROUND_CHOICES_BY_KEY.queens_conservatory
            or DEV_BACKGROUND_CHOICES_BY_KEY.exiles_reach
            or DEV_BACKGROUND_CHOICES_BY_KEY.murloc_shop
    end

    local applied = false
    local usingModel = false
    habitatBG:SetTexture(nil)
    habitatBG:SetTexCoord(0, 1, 0, 1)
    if habitatModelBG then
        habitatModelBG:Hide()
    end

    if entry and entry.modelFileID and habitatModelBG then
        local ok = pcall(habitatModelBG.SetModel, habitatModelBG, entry.modelFileID)
        if ok then
            usingModel = true
            applied = true
            habitatModelBG:Show()
        end
    end

    if (not applied) and entry and entry.atlas then
        local ok = pcall(habitatBG.SetAtlas, habitatBG, entry.atlas, true)
        applied = ok and habitatBG:GetTexture() ~= nil
    end
    if (not applied) and entry and entry.textures then
        for _, texturePath in ipairs(entry.textures) do
            habitatBG:SetTexture(texturePath)
            habitatBG:SetTexCoord(0, 1, 0, 1)
            if habitatBG:GetTexture() then
                applied = true
                break
            end
        end
    end
    if (not applied) then
        if not habitatBG:SetAtlas("shop-bg-environment-unavailable-murloc", true) then
            habitatBG:SetTexture("Interface\\Shop\\CatalogShopUnavailableMurloc")
            habitatBG:SetTexCoord(0, 1, 0, 1)
        end
    end

    habitatBG._mmBackgroundActive = not usingModel
    habitatBG:SetShown(not usingModel)
    if habitatModelBG then
        habitatModelBG._mmBackgroundActive = usingModel
    end
    if habitatShade then
        habitatShade:SetShown(true)
    end
end

local function ApplyHabitatBackgroundPreviewByChoice(previewTexture, choiceKey)
    if not previewTexture then
        return
    end

    local entry = DEV_BACKGROUND_CHOICES_BY_KEY[choiceKey]
    if not entry or choiceKey == "auto" then
        entry = DEV_BACKGROUND_CHOICES_BY_KEY.queens_conservatory
            or DEV_BACKGROUND_CHOICES_BY_KEY.exiles_reach
            or DEV_BACKGROUND_CHOICES_BY_KEY.murloc_shop
    end

    previewTexture:SetTexture(nil)
    previewTexture:SetTexCoord(0, 1, 0, 1)
    local applied = false

    if entry and entry.previewAtlas then
        local ok = pcall(previewTexture.SetAtlas, previewTexture, entry.previewAtlas, false)
        applied = ok and previewTexture:GetTexture() ~= nil
    end
    if (not applied) and entry and entry.atlas then
        local ok = pcall(previewTexture.SetAtlas, previewTexture, entry.atlas, false)
        applied = ok and previewTexture:GetTexture() ~= nil
    end
    if (not applied) and entry and entry.textures then
        for _, texturePath in ipairs(entry.textures) do
            previewTexture:SetTexture(texturePath)
            previewTexture:SetTexCoord(0, 1, 0, 1)
            if previewTexture:GetTexture() then
                applied = true
                break
            end
        end
    end
    if (not applied) then
        if not previewTexture:SetAtlas("shop-bg-environment-unavailable-murloc", false) then
            previewTexture:SetTexture("Interface\\Shop\\CatalogShopUnavailableMurloc")
            previewTexture:SetTexCoord(0, 1, 0, 1)
        end
    end
end

local ACTIVITY_ITEM_NEED_LOOKUP_CACHE = nil
local ACTIVITY_COLOR_CACHE = {}
local ACTIVITY_COLOR_CACHE_COUNT = 0
local ACTIVITY_COLOR_CACHE_MAX = 2048
local ACTIVITY_HISTORY_RENDER_LIMIT = 300
local STORE_SECTION_ENTRIES_CACHE = {}
local STORE_SECTION_ENTRIES_CACHE_COUNT = 0
local STORE_SECTION_ENTRIES_CACHE_MAX = 64

local function CacheActivityColor(cacheKey, color)
    if cacheKey ~= "" and ACTIVITY_COLOR_CACHE[cacheKey] == nil then
        ACTIVITY_COLOR_CACHE_COUNT = ACTIVITY_COLOR_CACHE_COUNT + 1
        if ACTIVITY_COLOR_CACHE_COUNT > ACTIVITY_COLOR_CACHE_MAX then
            ACTIVITY_COLOR_CACHE = {}
            ACTIVITY_COLOR_CACHE_COUNT = 0
        end
    end
    if cacheKey ~= "" then
        ACTIVITY_COLOR_CACHE[cacheKey] = color
    end
    return color
end

local function GetNeedColorByKey(needKey)
    if needKey == "hunger" then
        return COLOR_HUNGER
    end
    if needKey == "joy" then
        return COLOR_JOY
    end
    if needKey == "energy" then
        return COLOR_ENERGY
    end
    if needKey == "cleanliness" then
        return COLOR_CLEAN
    end
    return COLOR_WHITE
end

local function GetActivityEmoteColor()
    local emote = ChatTypeInfo and ChatTypeInfo.EMOTE
    if emote and emote.r and emote.g and emote.b then
        return { emote.r, emote.g, emote.b }
    end
    return { 1.0, 0.5, 0.25 }
end

local function GetActivityEntryLine(addon, entry)
    if addon and addon.GetActivityEntryLine then
        return addon:GetActivityEntryLine(entry)
    end
    if type(entry) == "table" then
        return tostring(entry.line or entry.message or "")
    end
    return tostring(entry or "")
end

local function StripActivityLinePrefix(line)
    local text = tostring(line or "")
    text = text:gsub("|c%x%x%x%x%x%x%x%x", "")
    text = text:gsub("|r", "")
    text = text:gsub("^%[%d%d:%d%d%]%s*", "")
    return text
end

local function ContainsAnyToken(text, tokens)
    if text == "" or type(tokens) ~= "table" then
        return false
    end
    for _, token in ipairs(tokens) do
        if text:find(token, 1, true) then
            return true
        end
    end
    return false
end

local function TrimActivityLabel(value)
    return tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "")
end

local function BuildPositiveCountSignature(byKey)
    local parts = {}
    for key, amount in pairs(byKey or {}) do
        local value = tonumber(amount) or 0
        if value > 0 then
            parts[#parts + 1] = tostring(key) .. ":" .. tostring(math.floor(value + 0.5))
        end
    end
    table.sort(parts)
    return table.concat(parts, ",")
end

local function GetCachedStoreSectionEntries(addon, sectionKey)
    local selectedSection = tostring(sectionKey or "food")
    local shouldPushOwnedToBottom = (selectedSection == "trinket" or selectedSection == "habitat")
    local signatureParts = { selectedSection }

    if shouldPushOwnedToBottom and addon and addon.GetStoreCatalog and addon.GetStoreItemCount then
        for _, entry in ipairs(addon:GetStoreCatalog() or {}) do
            if entry.category == selectedSection then
                local owned = (addon:GetStoreItemCount(entry.key) or 0) > 0 and "1" or "0"
                local effectivePrice = (addon.GetStoreEntryEffectivePrice and addon:GetStoreEntryEffectivePrice(entry)) or tonumber(entry.price) or 0
                signatureParts[#signatureParts + 1] = string.format("%s:%s:%s", tostring(entry.key or ""), owned, tostring(effectivePrice))
            end
        end
    end

    local signature = table.concat(signatureParts, "|")
    local cached = STORE_SECTION_ENTRIES_CACHE[selectedSection]
    if cached and cached.signature == signature and type(cached.entries) == "table" then
        return cached.entries
    end

    local entries = {}
    if selectedSection == "food" then
        for _, food in ipairs((addon and addon.GetFoodItems and addon:GetFoodItems()) or {}) do
            entries[#entries + 1] = {
                source = "food",
                key = food.key,
                label = food.label,
                icon = food.icon,
                price = food.price,
                hunger = food.hunger,
                joy = food.joy,
            }
        end
    elseif addon and addon.GetStoreCatalog then
        for _, entry in ipairs(addon:GetStoreCatalog() or {}) do
            if entry.category == selectedSection then
                entries[#entries + 1] = {
                    source = "store",
                    key = entry.key,
                    label = entry.label,
                    icon = entry.icon,
                    price = (addon.GetStoreEntryEffectivePrice and addon:GetStoreEntryEffectivePrice(entry)) or entry.price,
                    description = entry.description,
                    maxOwned = entry.maxOwned,
                    _entry = entry,
                }
            end
        end
    end

    table.sort(entries, function(a, b)
        if shouldPushOwnedToBottom and a.source == "store" and b.source == "store" then
            local aOwned = (addon:GetStoreItemCount(a.key) or 0) > 0
            local bOwned = (addon:GetStoreItemCount(b.key) or 0) > 0
            if aOwned ~= bOwned then
                return not aOwned
            end
        end
        local ap = tonumber(a.price) or 0
        local bp = tonumber(b.price) or 0
        if ap == bp then
            return (a.label or a.key or "") < (b.label or b.key or "")
        end
        return ap < bp
    end)

    if STORE_SECTION_ENTRIES_CACHE[selectedSection] == nil then
        STORE_SECTION_ENTRIES_CACHE_COUNT = STORE_SECTION_ENTRIES_CACHE_COUNT + 1
        if STORE_SECTION_ENTRIES_CACHE_COUNT > STORE_SECTION_ENTRIES_CACHE_MAX then
            STORE_SECTION_ENTRIES_CACHE = {}
            STORE_SECTION_ENTRIES_CACHE_COUNT = 0
        end
    end
    STORE_SECTION_ENTRIES_CACHE[selectedSection] = {
        signature = signature,
        entries = entries,
    }
    return entries
end

local ACTIVITY_NEED_PERCENT_TOKENS = {
    { token = "hunger", key = "hunger" },
    { token = "joy", key = "joy" },
    { token = "energy", key = "energy" },
    { token = "rest", key = "energy" },
    { token = "cleanliness", key = "cleanliness" },
    { token = "clean", key = "cleanliness" },
}

local function GetNeedPercentColorFromText(text)
    local lower = string.lower(tostring(text or ""))
    if lower == "" then
        return nil
    end

    local positiveNeed = nil
    for _, entry in ipairs(ACTIVITY_NEED_PERCENT_TOKENS) do
        local tokenPattern = "%f[%a]" .. entry.token .. "%f[%A]"
        local sign = lower:match("([%+%-])%s*%d+%.?%d*%%?%s*" .. tokenPattern)
        if not sign then
            sign = lower:match(tokenPattern .. "%s*([%+%-])%s*%d+%.?%d*%%?")
        end
        if sign == "-" then
            return COLOR_NEGATIVE
        end
        if sign == "+" and not positiveNeed then
            positiveNeed = entry.key
        end
    end

    if positiveNeed then
        return GetNeedColorByKey(positiveNeed)
    end
    return nil
end

local function BuildActivityItemNeedLookup(addon)
    if ACTIVITY_ITEM_NEED_LOOKUP_CACHE then
        return ACTIVITY_ITEM_NEED_LOOKUP_CACHE
    end

    local lookup = {}
    if addon and addon.GetFoodItems then
        for _, food in ipairs(addon:GetFoodItems() or {}) do
            local label = string.lower(TrimActivityLabel(food.label))
            if label ~= "" then
                lookup[label] = "hunger"
            end
        end
    end
    if addon and addon.GetStoreCatalog then
        for _, entry in ipairs(addon:GetStoreCatalog() or {}) do
            local label = string.lower(TrimActivityLabel(entry.label))
            if label ~= "" then
                if entry.category == "play" then
                    lookup[label] = "joy"
                elseif entry.category == "rest" then
                    lookup[label] = "energy"
                elseif entry.category == "clean" then
                    lookup[label] = "cleanliness"
                end
            end
        end
    end

    ACTIVITY_ITEM_NEED_LOOKUP_CACHE = lookup
    return lookup
end

local function ResolveItemNeedFromActivityText(addon, text)
    local lower = string.lower(tostring(text or ""))
    if lower == "" then
        return nil
    end

    local lookup = BuildActivityItemNeedLookup(addon)
    for label, needKey in pairs(lookup) do
        if lower:find(label, 1, true) then
            return needKey
        end
    end

    if lower:find("found food:", 1, true) then
        return "hunger"
    end
    return nil
end

local function GetActivityLineColor(addon, entry)
    local line = GetActivityEntryLine(addon, entry)
    local source = (type(entry) == "table") and string.lower(tostring(entry.source or "")) or ""
    local tone = (type(entry) == "table") and string.lower(tostring(entry.tone or "")) or ""
    local explicitNeed = (type(entry) == "table") and string.lower(tostring(entry.need or "")) or ""
    local text = string.lower(StripActivityLinePrefix(line))
    local cacheKey = source .. "\31" .. tone .. "\31" .. explicitNeed .. "\31" .. text
    local cached = ACTIVITY_COLOR_CACHE[cacheKey]
    if cached then
        return cached
    end
    local function finalize(color)
        return CacheActivityColor(cacheKey, color)
    end

    -- Need-percent updates always win over shell/item-only cues.
    local needPercentColor = GetNeedPercentColorFromText(text)
    if needPercentColor then
        return finalize(needPercentColor)
    end

    if tone == "loss" then
        return finalize(COLOR_NEGATIVE)
    elseif tone == "shell_gain" or tone == "shell" then
        return finalize(COLOR_SHELL)
    elseif tone == "item_gain" then
        return finalize(GetNeedColorByKey(explicitNeed))
    elseif tone == "benign" then
        return finalize(GetActivityEmoteColor())
    elseif tone == "system" then
        return finalize(COLOR_WHITE)
    end

    local hasShellWord = text:find("shell", 1, true) ~= nil
    local hasPercent = text:find("%%", 1, true) ~= nil
    local itemNeed = (explicitNeed ~= "") and explicitNeed or ResolveItemNeedFromActivityText(addon, text)

    local itemLoss = ContainsAnyToken(text, {
        "lost",
        "losing",
        "broken",
        "broke",
        "snapped",
        "used up",
        "wore down",
        "wore out",
        "split",
        "cracked",
        "popped",
        "tore",
        "rolled away",
        "drifted away",
        "frayed",
        "flickered out",
        "dissolved",
        "crumbled",
        "misplac",
    })
    local shellLoss = ContainsAnyToken(text, { "losing ", "lost ", "spent ", "bought ", "purchased ", "reloaded challenge" })
    if itemLoss or shellLoss then
        return finalize(COLOR_NEGATIVE)
    end

    local randomItemGain = itemNeed ~= nil and ContainsAnyToken(
        text,
        { "found food:", "found item:", "picked up", "surfaced with", "dug up", "cache", "plus " }
    )
    if randomItemGain then
        return finalize(GetNeedColorByKey(itemNeed))
    end

    local shellGainOnly = hasShellWord
        and (not hasPercent)
        and (not itemNeed)
        and ContainsAnyToken(text, { "gaining ", "gained ", "found ", "paid ", "earned ", "worth ", "haul", "plus " })
        and (not ContainsAnyToken(text, { "need ", "cost ", "for ", "buy " }))
    if shellGainOnly then
        return finalize(COLOR_SHELL)
    end

    if source == "autonomous" or source == "benign" then
        return finalize(GetActivityEmoteColor())
    end

    return finalize(COLOR_WHITE)
end

local function GetNeedColorByCategory(category)
    if category == "food" or category == "feed" then
        return COLOR_HUNGER
    end
    if category == "play" then
        return COLOR_JOY
    end
    if category == "rest" then
        return COLOR_ENERGY
    end
    if category == "clean" then
        return COLOR_CLEAN
    end
    return COLOR_WHITE
end

local function GetCareSectionButtonColors(_, selected)
    if selected then
        return 0.92, 0.16, 0.16
    end
    return 0.42, 0.08, 0.08
end

local function ApplyCareSectionTextColor(button, _, selected)
    if not button or not button.GetFontString then
        return
    end
    local fs = button:GetFontString()
    if not fs then
        return
    end
    local sectionKey = button._mmSectionKey
    local base = GetNeedColorByCategory(sectionKey)
    local scale = selected and 1.00 or 0.82
    fs:SetTextColor((base[1] or 1) * scale, (base[2] or 1) * scale, (base[3] or 1) * scale)
end

local function GetNeedColorByItemSourceAndKey(addon, source, key)
    if source == "food" then
        return GetNeedColorByCategory("food")
    end
    if addon and source == "store" and key then
        local entry = addon:GetStoreEntry(key)
        if entry then
            return GetNeedColorByCategory(entry.category)
        end
    end
    return COLOR_WHITE
end

local function ColorizeLabel(label, color)
    local r, g, b = unpack(color or COLOR_WHITE)
    local rr = math.floor((r or 1) * 255 + 0.5)
    local gg = math.floor((g or 1) * 255 + 0.5)
    local bb = math.floor((b or 1) * 255 + 0.5)
    return string.format("|cff%02x%02x%02x%s|r", rr, gg, bb, tostring(label or ""))
end

local function ColorizeShellText(label)
    return ColorizeLabel(label, COLOR_SHELL)
end

local function GetBasicActionDisplayGain(addon, sectionKey)
    local key = tostring(sectionKey or "")
    local gain = BASIC_ACTION_UI_VALUE

    if key == "feed" then
        local foodMultiplier = addon:GetFoodEffectMultiplier()
        local hungerFlat = addon:HasStoreUpgrade("tideLadle") and 4 or 0
        gain = (BASIC_ACTION_UI_VALUE * foodMultiplier) + hungerFlat
    elseif key == "play" then
        local playMultiplier = addon:HasStoreUpgrade("playBell") and 1.10 or 1.0
        gain = BASIC_ACTION_UI_VALUE * playMultiplier
    elseif key == "rest" then
        local restMultiplier = addon:HasStoreUpgrade("napWhistle") and 1.10 or 1.0
        gain = BASIC_ACTION_UI_VALUE * restMultiplier
    elseif key == "clean" then
        local cleanMultiplier = addon:HasStoreUpgrade("washStation") and 1.20 or 1.0
        gain = BASIC_ACTION_UI_VALUE * cleanMultiplier
    end

    return math.max(0, math.floor(gain + 0.5))
end

local function AddNeedDeltaPart(parts, value, needKey)
    local n = tonumber(value) or 0
    if n == 0 then
        return
    end
    parts[#parts + 1] = FormatNeedDelta(n, needKey)
end

local function BuildEmptyNeedDeltaTable()
    return {
        hunger = 0,
        joy = 0,
        energy = 0,
        cleanliness = 0,
    }
end

local function MergeNeedDeltaTables(base, extra)
    local out = BuildEmptyNeedDeltaTable()
    for _, needKey in ipairs(NEED_DELTA_ORDER) do
        out[needKey] = (tonumber(base and base[needKey]) or 0) + (tonumber(extra and extra[needKey]) or 0)
    end
    return out
end

local function BuildNeedDeltaParts(deltaMap)
    local parts = {}
    for _, needKey in ipairs(NEED_DELTA_ORDER) do
        AddNeedDeltaPart(parts, deltaMap and deltaMap[needKey], needKey)
    end
    return parts
end

local function BuildStoreNeedEffectDescription(prefix, totalDeltaMap, traitDeltaMap)
    local totalParts = BuildNeedDeltaParts(totalDeltaMap)
    if #totalParts <= 0 then
        return nil
    end
    local text = string.format("%s%s", tostring(prefix or ""), table.concat(totalParts, " "))
    local traitParts = BuildNeedDeltaParts(traitDeltaMap)
    if #traitParts > 0 then
        text = string.format("%s  T:%s", text, table.concat(traitParts, " "))
    end
    return text
end

local function GetPrimaryNeedKeyForTooltipSource(source, sectionKey, entryCategory)
    local src = tostring(source or "")
    if src == "food" then
        return "hunger"
    end
    if src == "basic" then
        local section = tostring(sectionKey or "")
        if section == "feed" then
            return "hunger"
        elseif section == "play" then
            return "joy"
        elseif section == "rest" then
            return "energy"
        elseif section == "clean" then
            return "cleanliness"
        end
    end
    if src == "store" then
        if entryCategory == "play" then
            return "joy"
        elseif entryCategory == "rest" then
            return "energy"
        elseif entryCategory == "clean" then
            return "cleanliness"
        end
    end
    return nil
end

local function GetTierLabelFromValue(tierValue)
    local tier = tonumber(tierValue)
    if not tier then
        return nil
    end
    tier = math.max(1, math.min(5, math.floor(tier + 0.5)))
    return string.format("Tier %d", tier)
end

local function ResolveRewardTierLabel(addon, itemKey)
    if not addon or not addon.GetRewardItemTierLookup or not itemKey then
        return nil
    end
    local lookup = addon:GetRewardItemTierLookup() or {}
    return GetTierLabelFromValue(lookup[itemKey])
end

local function BuildCompactNeedEffectText(deltaMap, preferredNeedKey)
    local parts = {}
    local used = {}

    if preferredNeedKey and (tonumber(deltaMap and deltaMap[preferredNeedKey]) or 0) ~= 0 then
        AddNeedDeltaPart(parts, deltaMap[preferredNeedKey], preferredNeedKey)
        used[preferredNeedKey] = true
    end

    local bestNegativeKey
    for _, needKey in ipairs(NEED_DELTA_ORDER) do
        if not used[needKey] then
            local value = tonumber(deltaMap and deltaMap[needKey]) or 0
            if value < 0 then
                bestNegativeKey = needKey
                break
            end
        end
    end
    if bestNegativeKey and #parts < 2 then
        AddNeedDeltaPart(parts, deltaMap[bestNegativeKey], bestNegativeKey)
        used[bestNegativeKey] = true
    end

    for _, needKey in ipairs(NEED_DELTA_ORDER) do
        if #parts >= 2 then
            break
        end
        if not used[needKey] then
            AddNeedDeltaPart(parts, deltaMap and deltaMap[needKey], needKey)
        end
    end

    return table.concat(parts, " ")
end

local function BuildPrimaryNeedEffectText(deltaMap, preferredNeedKey)
    if preferredNeedKey and (tonumber(deltaMap and deltaMap[preferredNeedKey]) or 0) ~= 0 then
        return FormatNeedDelta(deltaMap[preferredNeedKey], preferredNeedKey)
    end
    for _, needKey in ipairs(NEED_DELTA_ORDER) do
        local value = tonumber(deltaMap and deltaMap[needKey]) or 0
        if value ~= 0 then
            return FormatNeedDelta(value, needKey)
        end
    end
    return ""
end

local function HideRegion(region)
    if region and region.Hide then
        region:Hide()
    end
end

local function StripCollectionsFiligree(frame)
    -- Keep the tiled background; remove ornate corners/shadows for a flatter look.
    HideRegion(frame.ShadowCornerTopLeft)
    HideRegion(frame.ShadowCornerTopRight)
    HideRegion(frame.ShadowCornerBottomLeft)
    HideRegion(frame.ShadowCornerBottomRight)
    HideRegion(frame.ShadowCornerTop)
    HideRegion(frame.ShadowCornerLeft)
    HideRegion(frame.ShadowCornerRight)
    HideRegion(frame.ShadowCornerBottom)

    HideRegion(frame.OverlayShadowTopLeft)
    HideRegion(frame.OverlayShadowTopRight)
    HideRegion(frame.OverlayShadowBottomLeft)
    HideRegion(frame.OverlayShadowBottomRight)
    HideRegion(frame.OverlayShadowTop)
    HideRegion(frame.OverlayShadowLeft)
    HideRegion(frame.OverlayShadowRight)
    HideRegion(frame.OverlayShadowBottom)

    HideRegion(frame.BGCornerTopLeft)
    HideRegion(frame.BGCornerTopRight)
    HideRegion(frame.BGCornerBottomLeft)
    HideRegion(frame.BGCornerBottomRight)
end

local function CreateCard(parent, width, height, point, relativeTo, relativePoint, x, y)
    local ok, card = pcall(CreateFrame, "Frame", nil, parent, "CollectionsBackgroundTemplate")
    if not ok or not card then
        card = CreateFrame("Frame", nil, parent, "InsetFrameTemplate3")
    end
    card:ClearAllPoints()
    card:SetSize(width, height)
    card:SetPoint(point, relativeTo, relativePoint, x, y)
    StripCollectionsFiligree(card)
    return card
end

local function CreateOutlinePulseFrame(parent, edgeSize, inset)
    local glow = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    glow:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4)
    glow:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4)
    glow:SetFrameStrata(parent:GetFrameStrata())
    glow:SetFrameLevel((parent:GetFrameLevel() or 1) + 40)
    glow:SetToplevel(true)
    glow:SetBackdrop({
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = edgeSize or 10,
        insets = {
            left = inset or 3,
            right = inset or 3,
            top = inset or 3,
            bottom = inset or 3,
        },
    })
    glow:SetBackdropColor(0, 0, 0, 0)
    glow:SetBackdropBorderColor(1.0, 0.86, 0.14, 1.0)
    glow:SetAlpha(0)
    return glow
end

local function SkinButton(button)
    button:SetNormalFontObject("GameFontHighlightSmall")
    button:SetHighlightFontObject("GameFontHighlightSmall")
    button:SetDisabledFontObject("GameFontDisableSmall")
    if button.Left then
        button.Left:SetAlpha(0.7)
    end
    if button.Middle then
        button.Middle:SetAlpha(0.7)
    end
    if button.Right then
        button.Right:SetAlpha(0.7)
    end
    if button.LeftDisabled then
        button.LeftDisabled:SetAlpha(0.55)
    end
    if button.MiddleDisabled then
        button.MiddleDisabled:SetAlpha(0.55)
    end
    if button.RightDisabled then
        button.RightDisabled:SetAlpha(0.55)
    end
end

local function SkinSetListButton(button)
    local bg = button:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(true)
    if not bg:SetAtlas("PetList-ButtonBackground", true) then
        bg:SetColorTexture(0.1, 0.14, 0.2, 0.8)
    end
    bg:SetAlpha(0.9)

    local selected = button:CreateTexture(nil, "ARTWORK")
    selected:SetAllPoints(true)
    if not selected:SetAtlas("PetList-ButtonSelect", true) then
        selected:SetColorTexture(0.2, 0.32, 0.48, 0.35)
    end
    selected:SetAlpha(0.45)
    selected:Hide()

    local highlight = button:CreateTexture(nil, "HIGHLIGHT")
    highlight:SetAllPoints(true)
    if not highlight:SetAtlas("PetList-ButtonHighlight", true) then
        highlight:SetColorTexture(0.55, 0.72, 1, 0.2)
    end
    highlight:SetBlendMode("ADD")

    button._selectedTexture = selected
end

local function CreateTopTabButton(parent)
    local ok, tab = pcall(CreateFrame, "Button", nil, parent, "PanelTopTabButtonTemplate")
    if ok and tab then
        tab:SetNormalFontObject("GameFontNormalSmall")
        tab:SetHighlightFontObject("GameFontHighlightSmall")
        tab:SetDisabledFontObject("GameFontDisableSmall")
        return tab
    end

    ok, tab = pcall(CreateFrame, "Button", nil, parent, "PanelTabButtonTemplate")
    if ok and tab then
        tab:SetNormalFontObject("GameFontNormalSmall")
        tab:SetHighlightFontObject("GameFontHighlightSmall")
        tab:SetDisabledFontObject("GameFontDisableSmall")
        return tab
    end

    tab = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    SkinButton(tab)
    return tab
end

local function CreateBottomOptionToggle(parent, relativeTo, xOffset, labelText, onClick)
    local toggle = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
    toggle:SetSize(22, 22)
    toggle:SetPoint("RIGHT", relativeTo, "LEFT", xOffset, 0)

    toggle.Label = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    toggle.Label:SetPoint("RIGHT", toggle, "LEFT", -2, 0)
    toggle.Label:SetText(labelText)
    toggle.Label:SetTextColor(0.9, 0.85, 0.7)
    toggle:SetScript("OnClick", onClick)
    return toggle
end

local function BuildStatusBar(parent, label, yOffset, color)
    local row = CreateFrame("Frame", nil, parent)
    row:SetPoint("TOPLEFT", parent, "TOPLEFT", 16, yOffset)
    row:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -16, yOffset)
    row:SetHeight(24)

    local text = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    text:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
    text:SetText(label)

    local valueText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    valueText:SetPoint("TOPRIGHT", row, "TOPRIGHT", 0, 0)

    local bar = CreateFrame("StatusBar", nil, row)
    bar:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
    bar:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
    bar:SetHeight(12)
    bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
    bar:SetMinMaxValues(0, 100)

    local r, g, b = unpack(color)
    bar:SetStatusBarColor(r, g, b)

    local bg = bar:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(true)
    bg:SetColorTexture(0.08, 0.1, 0.14, 0.95)

    row.bar = bar
    row.valueText = valueText
    return row
end

local function CreateQuickBuyRow(parent, item, index)
    local availableWidth = math.max(220, parent:GetWidth() - 24)
    local rowWidth = math.floor((availableWidth - 8) / 2)
    local col = (index - 1) % 2
    local rowIndex = math.floor((index - 1) / 2)
    local row = CreateFrame("Frame", nil, parent)
    row:SetSize(rowWidth, 40)
    row:SetPoint("TOPLEFT", parent, "TOPLEFT", 12 + (col * (rowWidth + 8)), -34 - (rowIndex * 44))

    local rowBG = row:CreateTexture(nil, "BACKGROUND")
    rowBG:SetAllPoints(true)
    rowBG:SetColorTexture(0.08, 0.13, 0.18, 0.45)

    local divider = row:CreateTexture(nil, "BORDER")
    divider:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
    divider:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
    divider:SetHeight(1)
    divider:SetColorTexture(0.16, 0.58, 0.72, 0.55)

    local icon = row:CreateTexture(nil, "ARTWORK")
    icon:SetSize(28, 28)
    icon:SetPoint("TOPLEFT", row, "TOPLEFT", 2, -2)
    icon:SetTexture(item.icon)

    local name = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    name:SetPoint("TOPLEFT", icon, "TOPRIGHT", 4, 0)
    name:SetPoint("TOPRIGHT", row, "TOPRIGHT", -48, -2)
    name:SetJustifyH("LEFT")
    name:SetText(item.label)

    local effect = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    effect:SetPoint("TOPRIGHT", row, "TOPRIGHT", -4, -2)
    effect:SetJustifyH("RIGHT")
    effect:SetText("")

    local owned = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    owned:SetPoint("TOPLEFT", name, "BOTTOMLEFT", 0, -2)
    owned:SetJustifyH("LEFT")

    local buy = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
    buy:SetSize(56, 21)
    buy:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", -4, 2)
    buy:SetText(ColorizeShellText(item.price .. " Shells"))
    SkinButton(buy)

    row.owned = owned
    row.buy = buy
    row.name = name
    row.effect = effect

    return row
end

local function CreateStoreDetailRow(parent, index)
    local row = CreateFrame("Frame", nil, parent)
    row:SetSize(parent:GetWidth() - 36, 36)
    row:SetPoint("TOPLEFT", parent, "TOPLEFT", 8, -26 - ((index - 1) * 38))

    local bg = row:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(true)
    if not bg:SetAtlas("PetList-ButtonBackground", true) then
        bg:SetColorTexture(0.1, 0.1, 0.1, 0.65)
    end
    bg:SetAlpha(0.7)

    local freeHighlight = row:CreateTexture(nil, "ARTWORK")
    freeHighlight:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
    freeHighlight:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
    freeHighlight:SetColorTexture(COLOR_HATCH[1], COLOR_HATCH[2], COLOR_HATCH[3], 0.18)
    freeHighlight:Hide()

    local icon = row:CreateTexture(nil, "ARTWORK")
    icon:SetSize(24, 24)
    icon:SetPoint("LEFT", row, "LEFT", 6, 0)

    local title = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    title:SetPoint("TOPLEFT", icon, "TOPRIGHT", 8, -2)
    title:SetWidth(330)
    title:SetJustifyH("LEFT")

    local desc = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    desc:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -2)
    desc:SetWidth(330)
    desc:SetJustifyH("LEFT")

    local owned = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    owned:SetPoint("RIGHT", row, "RIGHT", -130, 0)

    local buy = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
    buy:SetSize(112, 20)
    buy:SetPoint("RIGHT", row, "RIGHT", -8, 0)
    SkinButton(buy)

    row.Icon = icon
    row.Title = title
    row.Desc = desc
    row.Owned = owned
    row.Buy = buy
    row.FreeHighlight = freeHighlight
    return row
end

local function CreateHabitatCustomizationCard(parent, index)
    local columns = 3
    local gap = 5
    local inset = 10
    local cardWidth = math.floor((parent:GetWidth() - (inset * 2) - ((columns - 1) * gap)) / columns)
    local cardHeight = 190
    local col = (index - 1) % columns
    local row = math.floor((index - 1) / columns)

    local card = CreateFrame("Frame", nil, parent, "InsetFrameTemplate3")
    card:SetSize(cardWidth, cardHeight)
    card:SetPoint("TOPLEFT", parent, "TOPLEFT", inset + (col * (cardWidth + gap)), -46 - (row * (cardHeight + gap)))
    card:SetClipsChildren(true)

    local preview = card:CreateTexture(nil, "ARTWORK")
    preview:SetPoint("TOPLEFT", card, "TOPLEFT", 2, -2)
    preview:SetPoint("TOPRIGHT", card, "TOPRIGHT", -2, -2)
    preview:SetHeight(114)
    preview:SetTexCoord(0, 1, 0, 1)

    local previewShade = card:CreateTexture(nil, "OVERLAY")
    previewShade:SetAllPoints(preview)
    previewShade:SetColorTexture(0, 0, 0, 0.18)

    local divider = card:CreateTexture(nil, "BORDER")
    divider:SetPoint("TOPLEFT", preview, "BOTTOMLEFT", 0, 0)
    divider:SetPoint("TOPRIGHT", preview, "BOTTOMRIGHT", 0, 0)
    divider:SetHeight(1)
    divider:SetColorTexture(0.16, 0.58, 0.72, 0.55)

    local title = card:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    title:SetPoint("TOPLEFT", preview, "BOTTOMLEFT", 4, -6)
    title:SetPoint("TOPRIGHT", preview, "BOTTOMRIGHT", -4, -6)
    title:SetJustifyH("LEFT")

    local price = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    price:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -2)
    price:SetPoint("TOPRIGHT", title, "BOTTOMRIGHT", 0, -2)
    price:SetJustifyH("LEFT")

    local buy = CreateFrame("Button", nil, card, "UIPanelButtonTemplate")
    buy:SetSize(cardWidth - 14, 22)
    buy:SetPoint("BOTTOM", card, "BOTTOM", 0, 8)
    SkinButton(buy)

    local selected = card:CreateTexture(nil, "OVERLAY")
    selected:SetPoint("TOPLEFT", card, "TOPLEFT", 0, 0)
    selected:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", 0, 0)
    selected:SetColorTexture(COLOR_HATCH[1], COLOR_HATCH[2], COLOR_HATCH[3], 0.17)
    selected:Hide()

    card.Preview = preview
    card.Title = title
    card.Price = price
    card.Buy = buy
    card.Selected = selected
    return card
end

local function CreateLinearCooldownBar(button)
    local overlay = button:CreateTexture(nil, "OVERLAY")
    if not overlay:SetAtlas("PetList-ButtonSelect", true) then
        overlay:SetColorTexture(0.72, 0.1, 0.1, 0.45)
    end
    overlay:SetVertexColor(1, 0.24, 0.24, 0.75)
    overlay:SetBlendMode("BLEND")
    overlay:Hide()
    return overlay
end

local function TrimString(str)
    return str and str:match("^%s*(.-)%s*$") or ""
end

local function GetScaledCursorPosition()
    local scale = UIParent and UIParent.GetEffectiveScale and UIParent:GetEffectiveScale() or 1
    if scale <= 0 then
        scale = 1
    end
    local x, y = GetCursorPosition()
    return (x or 0) / scale, (y or 0) / scale
end

local function ReanchorFrameToSavedPoint(addon, frame)
    if not addon or not frame or not addon.db or not addon.db.profile or not addon.db.profile.frame then
        return
    end

    local profile = addon.db.profile.frame
    local point = profile.point or "CENTER"
    local relativePoint = profile.relativePoint or point
    local x = tonumber(profile.x) or 0
    local y = tonumber(profile.y) or 0

    frame:ClearAllPoints()
    frame:SetPoint(point, UIParent, relativePoint, x, y)

    local actualPoint, _, actualRelativePoint, actualX, actualY = frame:GetPoint(1)
    profile.point = actualPoint or point
    profile.relativePoint = actualRelativePoint or actualPoint or relativePoint
    profile.x = math.floor(actualX or x)
    profile.y = math.floor(actualY or y)
end

local function ReanchorTrackerToSavedPoint(addon, tracker)
    if not addon or not tracker or not addon.db or not addon.db.profile then
        return
    end

    addon.db.profile.ui = addon.db.profile.ui or {}
    local profile = addon.db.profile.ui
    local point = profile.trackerPoint or "TOPRIGHT"
    local x = tonumber(profile.trackerX)
    local y = tonumber(profile.trackerY)
    if x == nil or y == nil then
        x = -120
        y = -220
    end

    tracker:ClearAllPoints()
    tracker:SetPoint(point, UIParent, point, x, y)

    local actualPoint, _, _, actualX, actualY = tracker:GetPoint(1)
    profile.trackerPoint = actualPoint or point
    profile.trackerX = math.floor(actualX or x)
    profile.trackerY = math.floor(actualY or y)
end

function MM:NormalizeMainFrameScale(value)
    local v = tonumber(value) or 1.0
    v = self:Clamp(v, 0.70, 1.30)
    return self:Clamp(math.floor((v * 20) + 0.5) / 20, 0.70, 1.30)
end

function MM:ApplyMainFrameScale(value, skipRefresh)
    local snapped = self:NormalizeMainFrameScale(value)
    self.db.profile.frame.scale = snapped
    if self.frame then
        self.frame:SetScale(snapped)
        ReanchorFrameToSavedPoint(self, self.frame)
    end
    if not skipRefresh and self.frame then
        self:RefreshUI(true)
    end
    return snapped
end

function MM:NormalizeTrackerScale(value)
    local v = tonumber(value) or 1.0
    v = self:Clamp(v, 0.70, 1.30)
    return self:Clamp(math.floor((v * 20) + 0.5) / 20, 0.70, 1.30)
end

function MM:ApplyTrackerScale(value, skipRefresh)
    self.db.profile.ui = self.db.profile.ui or {}
    local snapped = self:NormalizeTrackerScale(value)
    self.db.profile.ui.trackerScale = snapped
    if self.frame and self.frame.TrackerFrame then
        self.frame.TrackerFrame:SetScale(snapped)
        ReanchorTrackerToSavedPoint(self, self.frame.TrackerFrame)
    end
    if not skipRefresh and self.frame then
        self:RefreshUI(true)
    end
    return snapped
end

function MM:GetModelStageKey()
    local pet = self:GetPet()
    if pet and pet.stage == "egg" then
        return "egg"
    end
    return "adolescent"
end

function MM:GetModelControlDefaults(stageKey)
    local key = (stageKey == "egg") and "egg" or "adolescent"
    return MODEL_STAGE_DEFAULTS[key]
end

function MM:GetScaleAwareModelBounds(stageKey, scale, cam)
    local defaults = self:GetModelControlDefaults(stageKey)
    local baseScale = tonumber(defaults and defaults.scale) or 1
    local baseCam = tonumber(defaults and defaults.cam) or 1
    local currentScale = tonumber(scale) or baseScale
    local currentCam = tonumber(cam) or baseCam
    local baseVisual = baseScale / math.max(0.01, baseCam)
    local currentVisual = currentScale / math.max(0.01, currentCam)
    local ratio = currentVisual / math.max(0.01, baseVisual)
    local inverse = 1 / math.max(0.01, ratio)

    local yEdge = MODEL_POS_Y_EDGE_BASE * inverse
    local zEdge = MODEL_POS_Z_EDGE_BASE * inverse
    yEdge = self:Clamp(yEdge, MODEL_POS_Y_EDGE_BASE * MODEL_EDGE_SCALE_MIN_FACTOR, MODEL_POS_Y_EDGE_BASE * MODEL_EDGE_SCALE_MAX_FACTOR)
    zEdge = self:Clamp(zEdge, MODEL_POS_Z_EDGE_BASE * MODEL_EDGE_SCALE_MIN_FACTOR, MODEL_POS_Z_EDGE_BASE * MODEL_EDGE_SCALE_MAX_FACTOR)
    return -yEdge, yEdge, -zEdge, zEdge
end

function MM:GetDepthDragCoefficient(stageKey, depthX)
    local defaults = self:GetModelControlDefaults(stageKey)
    local baseX = tonumber(defaults and defaults.x) or 0
    local currentX = tonumber(depthX) or baseX
    local nearRange = math.max(0.001, baseX - MODEL_POS_X_MIN)
    local farRange = math.max(0.001, MODEL_POS_X_MAX - baseX)
    local maxRange = math.max(nearRange, farRange)
    local nearT = self:Clamp((baseX - currentX) / nearRange, 0, 1)
    local farT = self:Clamp((currentX - baseX) / farRange, 0, 1)
    local distanceT = self:Clamp(math.abs(currentX - baseX) / maxRange, 0, 1)
    local coeff = 1
        + (farT * MODEL_DRAG_FAR_GAIN)
        - (nearT * MODEL_DRAG_NEAR_REDUCTION)
        + (distanceT * MODEL_DRAG_DISTANCE_GAIN)
    return self:Clamp(coeff, MODEL_DRAG_COEFF_MIN, MODEL_DRAG_COEFF_MAX)
end

function MM:ClampModelControlState(stageKey, state)
    local defaults = self:GetModelControlDefaults(stageKey)
    local out = state or {}
    out.x = self:Clamp(tonumber(out.x) or defaults.x, MODEL_POS_X_MIN, MODEL_POS_X_MAX)
    out.facing = tonumber(out.facing) or defaults.facing
    out.scale = self:Clamp(tonumber(out.scale) or defaults.scale, MODEL_SCALE_MIN, MODEL_SCALE_MAX)
    out.cam = self:Clamp(tonumber(out.cam) or defaults.cam, 0.8, 2.0)
    if MODEL_UNCLAMP_DRAG then
        out.y = self:Clamp(tonumber(out.y) or defaults.y, MODEL_POS_Y_MIN, MODEL_POS_Y_MAX)
        out.z = self:Clamp(tonumber(out.z) or defaults.z, MODEL_POS_Z_MIN, MODEL_POS_Z_MAX)
    else
        local yMin, yMax, zMin, zMax = self:GetScaleAwareModelBounds(stageKey, out.scale, out.cam)
        out.y = self:Clamp(tonumber(out.y) or defaults.y, yMin, yMax)
        out.z = self:Clamp(tonumber(out.z) or defaults.z, zMin, zMax)
    end
    return out
end

function MM:GetModelControlState(stageKey)
    local key = (stageKey == "egg") and "egg" or "adolescent"
    self.db.profile.ui = self.db.profile.ui or {}
    local schemaVersion = tonumber(self.db.profile.ui.modelControlsVersion) or 0
    if schemaVersion ~= MODEL_CONTROL_SCHEMA_VERSION then
        self.db.profile.ui.modelControls = {}
        self.db.profile.ui.modelControlsVersion = MODEL_CONTROL_SCHEMA_VERSION
    end
    self.db.profile.ui.modelControls = self.db.profile.ui.modelControls or {}
    local controls = self.db.profile.ui.modelControls
    if not MODEL_CONTROLS_ENABLED then
        local defaults = self:GetModelControlDefaults(key)
        controls[key] = {
            x = defaults.x,
            y = defaults.y,
            z = defaults.z,
            facing = defaults.facing,
            scale = defaults.scale,
            cam = defaults.cam,
        }
        return controls[key]
    end
    controls[key] = controls[key] or {}
    controls[key] = self:ClampModelControlState(key, controls[key])
    return controls[key]
end

function MM:IsModelControlCustomized(stageKey)
    if not MODEL_CONTROLS_ENABLED then
        return false
    end
    local key = (stageKey == "egg") and "egg" or "adolescent"
    local defaults = self:GetModelControlDefaults(key)
    local state = self:GetModelControlState(key)
    if not defaults or not state then
        return false
    end
    local epsilon = 0.0001
    return math.abs((state.x or 0) - (defaults.x or 0)) > epsilon
        or math.abs((state.y or 0) - (defaults.y or 0)) > epsilon
        or math.abs((state.z or 0) - (defaults.z or 0)) > epsilon
        or math.abs((state.facing or 0) - (defaults.facing or 0)) > epsilon
        or math.abs((state.scale or 0) - (defaults.scale or 0)) > epsilon
        or math.abs((state.cam or 0) - (defaults.cam or 0)) > epsilon
end

function MM:ApplyModelControlState(stageKey)
    if not self.frame or not self.frame.EggModel then
        return
    end
    local key = (stageKey == "egg") and "egg" or "adolescent"
    local model = self.frame.EggModel
    local state = self:GetModelControlState(key)
    pcall(model.SetFacing, model, state.facing)
    pcall(model.SetPosition, model, state.x, state.y, state.z)
    pcall(model.SetModelScale, model, state.scale)
    pcall(model.SetCamDistanceScale, model, state.cam)
end

function MM:ApplyCurrentModelControlState()
    local stageKey = self:GetModelStageKey()
    self:ApplyModelControlState(stageKey)
end

function MM:ResetCurrentModelControlState()
    local stageKey = self:GetModelStageKey()
    local defaults = self:GetModelControlDefaults(stageKey)
    local state = self:GetModelControlState(stageKey)
    state.x = defaults.x
    state.y = defaults.y
    state.z = defaults.z
    state.facing = defaults.facing
    state.scale = defaults.scale
    state.cam = defaults.cam
    self:ApplyModelControlState(stageKey)
    if stageKey == "egg" and self.frame and self.frame.EggModel and self.frame.EggModel:IsShown() and self.frame:IsShown() and self.activeTab == "Habitat" then
        self:StartEggWiggle()
    end
end

function MM:GetPetDisplayName()
    local pet = self:GetPet()
    local name = TrimString(pet and pet.name or "")
    if name ~= "" then
        return name
    end
    if pet and pet.stage == "egg" then
        return "Unhatched Egg"
    end
    return "Unnamed Murloc"
end

function MM:BuildStoreTooltipData(source, key, sectionKey)
    local data = {
        name = "Item",
        price = nil,
        baseDeltas = BuildEmptyNeedDeltaTable(),
        traitDeltas = BuildEmptyNeedDeltaTable(),
        deltas = BuildEmptyNeedDeltaTable(),
        description = "Coming soon.",
        isFreePurchase = false,
        traitTriggerKey = nil,
        entryCategory = nil,
        usageTag = nil,
        tierLabel = nil,
    }

    local function finalize(triggerKey)
        data.traitTriggerKey = triggerKey
        if triggerKey and self.GetTraitActionNeedDelta then
            local traitDelta = self:GetTraitActionNeedDelta(triggerKey, 1)
            for _, needKey in ipairs(NEED_DELTA_ORDER) do
                data.traitDeltas[needKey] = tonumber(traitDelta and traitDelta[needKey]) or 0
            end
        end
        data.deltas = MergeNeedDeltaTables(data.baseDeltas, data.traitDeltas)
        return data
    end

    if source == "food" then
        local item
        for _, food in ipairs(self:GetFoodItems()) do
            if food.key == key then
                item = food
                break
            end
        end
        if item then
            local foodMultiplier = self:GetFoodEffectMultiplier()
            local hungerFlat = self:HasStoreUpgrade("tideLadle") and 4 or 0
            local joyFlat = (self.GetFoodJoyDelta and self:GetFoodJoyDelta()) or 2
            local cleanDelta = (self.GetFoodCleanlinessDelta and self:GetFoodCleanlinessDelta()) or 0
            local shownHunger = math.floor(((item.hunger or 0) * foodMultiplier) + hungerFlat + 0.5)
            local shownJoy = joyFlat
            data.name = item.label or data.name
            data.price = tonumber(item.price)
            data.baseDeltas.hunger = shownHunger
            data.baseDeltas.joy = shownJoy
            data.baseDeltas.cleanliness = cleanDelta
            data.description = item.description or data.description
            data.usageTag = "Consumable"
            data.tierLabel = ResolveRewardTierLabel(self, item.key)
        end
        return finalize("feed")
    end

    if source == "basic" then
        data.usageTag = "Reusable"
        data.tierLabel = "Basic Tier"
        local section = tostring(sectionKey or "")
        if section == "feed" then
            data.name = "Basic Feed"
            data.baseDeltas.hunger = GetBasicActionDisplayGain(self, section)
            data.description = "Worms are better than nothing."
        elseif section == "play" then
            data.name = "Basic Play"
            data.baseDeltas.joy = GetBasicActionDisplayGain(self, section)
            data.baseDeltas.energy = -8
            data.baseDeltas.cleanliness = -4
            data.description = "A little splash-tag keeps the boredom away."
        elseif section == "rest" then
            data.name = "Basic Rest"
            data.baseDeltas.energy = GetBasicActionDisplayGain(self, section)
            data.baseDeltas.hunger = -4
            data.description = "A short tidepool nap to catch your breath."
        elseif section == "clean" then
            data.name = "Basic Clean"
            data.baseDeltas.cleanliness = GetBasicActionDisplayGain(self, section)
            data.baseDeltas.joy = (self.GetCleanJoyDelta and self:GetCleanJoyDelta()) or -2
            data.description = "A quick rinse beats staying muddy."
        end
        return finalize(section)
    end

    local entry = self:GetStoreEntry(key)
    if not entry then
        return finalize(nil)
    end

    data.name = entry.label or data.name
    data.price = (self.GetStoreEntryEffectivePrice and self:GetStoreEntryEffectivePrice(entry)) or tonumber(entry.price)
    data.description = entry.description or data.description
    data.entryCategory = entry.category
    if entry.category == "play" or entry.category == "rest" or entry.category == "clean" then
        data.usageTag = "Reusable"
        data.tierLabel = ResolveRewardTierLabel(self, entry.key)
    elseif TOOLTIP_CONSUMABLE_STORE_KEYS[entry.key] then
        data.usageTag = "Consumable"
    else
        data.usageTag = "Reusable"
    end
    if entry.key == "nameTag" and self.IsNameTagFirstPurchaseFree and self:IsNameTagFirstPurchaseFree() then
        data.isFreePurchase = true
        data.description = string.format("%s First use is free.", data.description or "")
    end

    local traitTriggerKey = nil
    if entry.category == "play" then
        local playMultiplier = self:HasStoreUpgrade("playBell") and 1.10 or 1.0
        data.baseDeltas.joy = math.floor(((tonumber(entry.joyBonus) or 0) * playMultiplier) + 0.5)
        data.baseDeltas.energy = -8
        data.baseDeltas.cleanliness = -4
        traitTriggerKey = "play"
    elseif entry.category == "rest" then
        local itemMultiplier = self:HasStoreUpgrade("cozyNest") and 1.20 or 1.0
        local restMultiplier = self:HasStoreUpgrade("napWhistle") and 1.10 or 1.0
        data.baseDeltas.energy = math.floor(((tonumber(entry.restBonus) or 0) * itemMultiplier * restMultiplier) + 0.5)
        data.baseDeltas.hunger = -4
        traitTriggerKey = "rest"
    elseif entry.category == "clean" then
        local cleanMultiplier = self:HasStoreUpgrade("washStation") and 1.20 or 1.0
        data.baseDeltas.cleanliness = math.floor(((tonumber(entry.cleanBonus) or 0) * cleanMultiplier) + 0.5)
        data.baseDeltas.joy = (self.GetCleanJoyDelta and self:GetCleanJoyDelta()) or -2
        traitTriggerKey = "clean"
    end

    return finalize(traitTriggerKey)
end

function MM:ShowStoreItemTooltip(owner, source, key, sectionKey)
    if not owner then
        return
    end
    local data = self:BuildStoreTooltipData(source, key, sectionKey)
    local baseParts = BuildNeedDeltaParts(data.baseDeltas)
    local traitParts = BuildNeedDeltaParts(data.traitDeltas)
    local totalParts = BuildNeedDeltaParts(data.deltas)

    GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
    local usageTagText = data.usageTag and string.format("[%s]", data.usageTag) or ""
    if usageTagText ~= "" then
        GameTooltip:AddDoubleLine(data.name or "Item", usageTagText, 1.0, 0.82, 0.0, 0.85, 0.85, 0.85)
    else
        GameTooltip:AddLine(data.name or "Item", 1.0, 0.82, 0.0, true)
    end
    if data.tierLabel and data.tierLabel ~= "" then
        GameTooltip:AddLine(data.tierLabel, 0.92, 0.92, 0.92, true)
    end
    GameTooltip:AddLine(data.description or "Coming soon.", 0.72, 0.72, 0.72, true)
    GameTooltip:AddLine(" ")
    GameTooltip:AddLine("Need Effects", 0.92, 0.92, 0.92)
    if #totalParts > 0 then
        if #traitParts > 0 then
            if #baseParts > 0 then
                GameTooltip:AddLine(string.format("Item: %s", table.concat(baseParts, "  ")), 1, 1, 1, true)
            end
            GameTooltip:AddLine(string.format("Traits: %s", table.concat(traitParts, "  ")), 0.78, 0.88, 1.0, true)
            GameTooltip:AddLine(string.format("Total: %s", table.concat(totalParts, "  ")), 1, 1, 1, true)
        else
            GameTooltip:AddLine(table.concat(totalParts, "  "), 1, 1, 1, true)
        end
    else
        GameTooltip:AddLine("No direct need changes.", 0.72, 0.72, 0.72, true)
    end
    if data.isFreePurchase then
        GameTooltip:AddLine(string.format("Price: |cff%02x%02x%02xFREE|r",
            math.floor((COLOR_HATCH[1] or 1) * 255 + 0.5),
            math.floor((COLOR_HATCH[2] or 1) * 255 + 0.5),
            math.floor((COLOR_HATCH[3] or 1) * 255 + 0.5)
        ), 1, 1, 1, true)
    elseif data.price and data.price > 0 then
        GameTooltip:AddLine(string.format("Price: %s", ColorizeShellText(string.format("%d shells", data.price))), 1, 1, 1, true)
    end
    GameTooltip:Show()
end

local function BuildTraitTooltipEffectsLine(addon, traitName)
    local traitLabel = TrimString(tostring(traitName or ""))
    if traitLabel == "" then
        return "None"
    end

    local segments = {}
    if addon and addon.GetTraitNeedDeltaForTrait then
        for _, triggerKey in ipairs(TRAIT_TOOLTIP_TRIGGER_ORDER) do
            local triggerDelta = addon:GetTraitNeedDeltaForTrait(traitLabel, triggerKey, 1)
            local parts = BuildNeedDeltaParts(triggerDelta)
            if #parts > 0 then
                local triggerLabel = TRAIT_TOOLTIP_TRIGGER_LABELS[triggerKey] or triggerKey
                segments[#segments + 1] = string.format("%s %s", triggerLabel, table.concat(parts, " "))
            end
        end
    end

    if #segments <= 0 then
        return traitLabel
    end
    return string.format("%s - %s", traitLabel, table.concat(segments, "; "))
end

local function BuildPetTooltipZoneLine(addon)
    if addon and addon.IsNeedFreezeInstanceContent then
        local frozenInInstance = addon:IsNeedFreezeInstanceContent()
        if frozenInInstance then
            return "Zone: Instance - Decay Frozen"
        end
    end
    if (IsResting and IsResting()) then
        return "Zone: Rested XP - 1/2 Decay rate"
    end
    return "Zone: Overworld - Full decay"
end

local function BuildPetTooltipScavengeLine(addon, pet)
    if not addon or not pet or pet.stage == "egg" then
        return "Scavenging: Unavailable"
    end

    if addon.IsNeedFreezeInstanceContent and addon:IsNeedFreezeInstanceContent() then
        return "Scavenging: Instance - Unavailable"
    end

    if (IsResting and IsResting()) then
        return "Scavenging: Rested XP - Unavailable"
    end

    local zone = GetRealZoneText()
    if addon.IsMajorCityZone and addon:IsMajorCityZone(zone) then
        return "Scavenging: Overworld - Unavailable"
    end

    local payout = addon.GetCurrentScavengeTickPayout and (tonumber(addon:GetCurrentScavengeTickPayout()) or 0) or 0
    local economy = addon.GetEconomy and addon:GetEconomy() or nil
    local work = economy and economy.work or {}
    if work.active then
        return string.format("Scavenging: Overworld - Active (+%d shells/tick)", payout)
    end

    local restartRemaining = addon.GetWorkStartCooldownRemaining and (tonumber(addon:GetWorkStartCooldownRemaining()) or 0) or 0
    if restartRemaining > 0 then
        return string.format("Scavenging: Overworld - Rejoin in %.0fs (+%d shells/tick)", restartRemaining, payout)
    end

    return string.format("Scavenging: Overworld - Available (+%d shells/tick)", payout)
end

function MM:ShowPetIdentityTooltip(owner)
    if not owner then
        return
    end

    local pet = self:GetPet()
    local petName = self:GetPetDisplayName()
    local hatchDescription = (self.GetDirectionalHatchMessage and self:GetDirectionalHatchMessage(pet)) or ""
    if pet and pet.stage == "egg" then
        hatchDescription = "I wonder what's inside?!"
    end
    local primaryTrait = TrimString(tostring((pet and pet.personality) or ""))
    local secondaryTrait = TrimString(tostring((pet and pet.personalitySecondary) or ""))
    local showSecondary = secondaryTrait ~= "" and string.lower(secondaryTrait) ~= string.lower(primaryTrait)

    GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
    GameTooltip:AddLine(petName, 1.0, 0.82, 0.0, true)
    if hatchDescription ~= "" then
        GameTooltip:AddLine(hatchDescription, 0.92, 0.92, 0.92, true)
    end
    if pet and pet.stage == "egg" then
        GameTooltip:Show()
        return
    end
    GameTooltip:AddLine(" ")
    GameTooltip:AddLine(BuildPetTooltipZoneLine(self), 0.92, 0.92, 0.92, true)
    GameTooltip:AddLine(BuildPetTooltipScavengeLine(self, pet), 0.92, 0.92, 0.92, true)
    GameTooltip:AddLine(" ")
    GameTooltip:AddLine(string.format("Trait 1: %s", BuildTraitTooltipEffectsLine(self, primaryTrait)), 1, 1, 1, true)
    if showSecondary then
        GameTooltip:AddLine(string.format("Trait 2: %s", BuildTraitTooltipEffectsLine(self, secondaryTrait)), 1, 1, 1, true)
    else
        GameTooltip:AddLine("Trait 2: None", 0.72, 0.72, 0.72, true)
    end
    GameTooltip:Show()
end

function MM:BindPetIdentityTooltip(region)
    if not region then
        return
    end
    if region.EnableMouse then
        region:EnableMouse(true)
    end
    region:SetScript("OnEnter", function(owner)
        self:ShowPetIdentityTooltip(owner)
    end)
    region:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
end

function MM:UpdateEggCooldownProgressBars()
    if not self.frame or not self.GetEggCareCooldown then
        return
    end

    local pet = self:GetPet()
    local isEgg = pet.stage == "egg"
    local entries = {
        { action = "soothe", button = self.frame.PlayActionButton, bar = self.frame.SootheCooldownBar },
        { action = "clean", button = self.frame.CleanActionButton, bar = self.frame.CleanEggCooldownBar },
        { action = "warm", button = self.frame.RestActionButton, bar = self.frame.WarmCooldownBar },
    }

    for _, entry in ipairs(entries) do
        if entry.bar then
            if isEgg then
                local remaining, duration = self:GetEggCareCooldown(entry.action)
                if duration > 0 and remaining > 0 then
                    local progress = (duration - remaining) / duration
                    progress = self:Clamp(progress, 0, 1)
                    local width = math.max(1, math.floor((entry.button and entry.button:GetWidth() or 1) * progress))
                    entry.bar:ClearAllPoints()
                    entry.bar:SetPoint("TOPLEFT", entry.button, "TOPLEFT", 0, 0)
                    entry.bar:SetPoint("BOTTOMLEFT", entry.button, "BOTTOMLEFT", 0, 0)
                    entry.bar:SetWidth(width)
                    entry.bar:Show()
                    if entry.button then
                        entry.button:SetEnabled(false)
                    end
                else
                    entry.bar:Hide()
                    if entry.button then
                        entry.button:SetEnabled(true)
                    end
                end
            else
                entry.bar:Hide()
            end
        end
    end
end

function MM:UpdateAdolescentCooldownProgressBars()
    if not self.frame or not self.frame.CareAccordion or not self.GetAdolescentActionCooldown then
        return
    end

    local pet = self:GetPet()
    local isAdolescentPlus = pet.stage ~= "egg"
    local accordion = self.frame.CareAccordion

    for _, section in pairs(accordion.sections or {}) do
        if section.cooldownBar and section.header then
            if isAdolescentPlus then
                local remaining, duration = self:GetAdolescentActionCooldown(section.key)
                if duration > 0 and remaining > 0 then
                    local progress = (duration - remaining) / duration
                    progress = self:Clamp(progress, 0, 1)
                    local width = math.max(1, math.floor(section.header:GetWidth() * progress))
                    section.cooldownBar:ClearAllPoints()
                    section.cooldownBar:SetPoint("TOPLEFT", section.header, "TOPLEFT", 0, 0)
                    section.cooldownBar:SetPoint("BOTTOMLEFT", section.header, "BOTTOMLEFT", 0, 0)
                    section.cooldownBar:SetWidth(width)
                    section.cooldownBar:Show()
                else
                    section.cooldownBar:Hide()
                end
            else
                section.cooldownBar:Hide()
            end
        end
    end
end

function MM:UpdateAdolescentCooldownLabels()
    if not self.frame or not self.frame.CareAccordion or not self.GetAdolescentActionCooldown then
        return
    end

    local pet = self:GetPet()
    if pet.stage == "egg" then
        return
    end

    self.db.profile.ui = self.db.profile.ui or {}
    local openSection = self.db.profile.ui.openCareSection or "feed"
    local accordion = self.frame.CareAccordion

    for _, sectionKey in ipairs(accordion.order or {}) do
        local section = accordion.sections and accordion.sections[sectionKey]
        if section and section.header and section.header:IsShown() then
            local expanded = openSection == sectionKey
            local remaining = self:GetAdolescentActionCooldown(sectionKey)
            section.header:SetEnabled(true)
            if section.header._selectedTexture then
                if expanded then
                    section.header._selectedTexture:Show()
                else
                    section.header._selectedTexture:Hide()
                end
            end
            if section.header.Left and section.header.Middle and section.header.Right then
                local r, g, b = GetCareSectionButtonColors(sectionKey, expanded)
                section.header.Left:SetVertexColor(r, g, b)
                section.header.Middle:SetVertexColor(r, g, b)
                section.header.Right:SetVertexColor(r, g, b)
            end
            ApplyCareSectionTextColor(section.header, sectionKey, expanded)
            if remaining > 0 then
                section.header:SetText(string.format("%s %.1f", section.label, remaining))
            else
                section.header:SetText(section.label)
            end

            if expanded then
                for _, row in ipairs(section.rows or {}) do
                    if row:IsShown() then
                        local hasItem = row.itemKey ~= nil
                        local count = row.itemCount or 0
                        local canUse = row.itemIsFree or count > 0
                        row:SetEnabled(hasItem and canUse and remaining <= 0)
                    end
                end
            end
        end
    end
end

function MM:UpdateTrackerBasicButtonCooldowns()
    if not self.frame or not self.frame.TrackerBasicButtons then
        return
    end

    local pet = self:GetPet()
    if not pet then
        return
    end

    for _, btn in pairs(self.frame.TrackerBasicButtons) do
        if btn and btn.IsShown and btn:IsShown() then
            local label = btn._mmLabel or "Basic"
            if pet.stage == "egg" and btn._mmEggAction and self.GetEggCareCooldown then
                local remaining = self:GetEggCareCooldown(btn._mmEggAction)
                if remaining > 0 then
                    btn:SetText(string.format("%s %.1f", label, remaining))
                    btn:SetEnabled(false)
                else
                    btn:SetText(label)
                    btn:SetEnabled(true)
                end
            else
                local sectionKey = btn._mmSection
                if sectionKey and self.GetAdolescentActionCooldown then
                    local gain = GetBasicActionDisplayGain(self, sectionKey)
                    local remaining = self:GetAdolescentActionCooldown(sectionKey)
                    if remaining > 0 then
                        btn:SetText(string.format("%s %.1f", label, remaining))
                        btn:SetEnabled(false)
                    else
                        btn:SetText(string.format("%s +%d%%", label, gain))
                        btn:SetEnabled(true)
                    end
                end
            end
        end
    end
end

function MM:UpdateRerollCountdown()
    if not self.frame or not self.frame.ChallengeRerollButton or not self.GetChallengeRerollRemaining then
        return
    end

    local pet = self:GetPet()
    local store = self:GetStore()
    local isEgg = pet.stage == "egg"

    if not isEgg then
        local shellCost = 20
        local canAfford = (store.shells or 0) >= shellCost
        self.frame.ChallengeRerollButton:SetText(string.format("Reroll (%d)", shellCost))
        self.frame.ChallengeRerollButton:SetEnabled(canAfford)
        self._lastRerollDisplaySeconds = nil
        return
    end

    local remaining = self:GetChallengeRerollRemaining()
    local displaySeconds = math.ceil(remaining)
    if self._lastRerollDisplaySeconds == displaySeconds then
        return
    end
    self._lastRerollDisplaySeconds = displaySeconds

    if remaining > 0 then
        local mins = math.floor(remaining / 60)
        local secs = math.floor(remaining % 60)
        self.frame.ChallengeRerollButton:SetText(string.format("Reroll (%d:%02d)", mins, secs))
        self.frame.ChallengeRerollButton:SetEnabled(false)
    else
        self.frame.ChallengeRerollButton:SetText("Reroll")
        self.frame.ChallengeRerollButton:SetEnabled(true)
    end
end

function MM:UpdateScavengeCountdown()
    if not self.frame or not self.frame.WorkStatusText then
        return
    end

    local pet = self:GetPet()
    local economy = self:GetEconomy()
    local restartRemaining = (self.GetWorkStartCooldownRemaining and self:GetWorkStartCooldownRemaining()) or 0
    local active = economy.work and economy.work.active
    local paused = economy.work and economy.work.pausedForSafeZone

    local statusText
    if pet.stage == "egg" then
        statusText = "Inactive"
        self.frame.WorkStatusText:SetTextColor(0.60, 0.60, 0.60)
    elseif paused then
        statusText = "Paused"
        self.frame.WorkStatusText:SetTextColor(1.0, 0.82, 0.1)
    elseif active then
        statusText = "Active"
        self.frame.WorkStatusText:SetTextColor(0.35, 1.00, 0.35)
    else
        statusText = "Inactive"
        self.frame.WorkStatusText:SetTextColor(0.60, 0.60, 0.60)
    end

    if self._lastWorkStatusText ~= statusText then
        self.frame.WorkStatusText:SetText(statusText)
        self._lastWorkStatusText = statusText
    end

    if not self.frame.WorkActionButton or pet.stage == "egg" then
        return
    end

    if economy.work and economy.work.active then
        if self._lastWorkButtonText ~= "Recall" then
            self.frame.WorkActionButton:SetText("Recall")
            self._lastWorkButtonText = "Recall"
        end
        self.frame.WorkActionButton:SetEnabled(true)
        return
    end

    if paused then
        if self._lastWorkButtonText ~= "Paused" then
            self.frame.WorkActionButton:SetText("Paused")
            self._lastWorkButtonText = "Paused"
        end
        self.frame.WorkActionButton:SetEnabled(false)
        return
    end

    if restartRemaining > 0 then
        local countdownText = string.format("Scavenge (%ds)", math.ceil(restartRemaining))
        if self._lastWorkButtonText ~= countdownText then
            self.frame.WorkActionButton:SetText(countdownText)
            self._lastWorkButtonText = countdownText
        end
        self.frame.WorkActionButton:SetEnabled(false)
    else
        if self._lastWorkButtonText ~= "Send To Scavenge" then
            self.frame.WorkActionButton:SetText("Send To Scavenge")
            self._lastWorkButtonText = "Send To Scavenge"
        end
        self.frame.WorkActionButton:SetEnabled(true)
    end
end

function MM:ShowNameMurlocPrompt()
    local pet = self:GetPet()
    if not pet or pet.stage == "egg" then
        return
    end
    if TrimString(pet.name or "") ~= "" then
        return
    end

    StaticPopupDialogs["MURLOC_MINDER_NAME_MURLOC"] = {
        text = "Your hatchling hatched! Choose a name:",
        button1 = "Save Name",
        hasEditBox = true,
        maxLetters = 24,
        timeout = 0,
        whileDead = true,
        hideOnEscape = true,
        OnShow = function(dialog)
            local editBox = dialog.EditBox or dialog.editBox
            if editBox then
                editBox:SetAutoFocus(true)
                editBox:SetText("")
                editBox:HighlightText()
            end
        end,
        OnAccept = function(dialog)
            local editBox = dialog.EditBox or dialog.editBox
            local input = TrimString(editBox and editBox:GetText() or "")
            if input == "" then
                input = "Mrrgl"
            end
            local addon = _G.MurlocMinder
            if addon then
                addon:GetPet().name = input
                addon:PushActivity("You named " .. input .. ".")
                addon:RefreshUI(true)
                if addon.PlayUISound then
                    addon:PlayUISound("popup_close")
                end
            end
        end,
        EditBoxOnEnterPressed = function(editBox)
            local parent = editBox:GetParent()
            if parent and parent.button1 and parent.button1:IsEnabled() then
                parent.button1:Click()
            end
        end,
    }

    if self.PlayUISound then
        self:PlayUISound("popup_open")
    end
    StaticPopup_Show("MURLOC_MINDER_NAME_MURLOC")
end

function MM:ShowRenameMurlocPrompt(skipNametagCheck)
    local pet = self:GetPet()
    if not pet or pet.stage == "egg" then
        self:PushActivity("Your egg has no name to change yet.")
        return
    end
    if not skipNametagCheck and not self:HasStoreUpgrade("nameTag") then
        self:PushActivity("Buy a Nametag in the Store to rename your murloc.")
        return
    end

    StaticPopupDialogs["MURLOC_MINDER_RENAME_MURLOC"] = StaticPopupDialogs["MURLOC_MINDER_RENAME_MURLOC"] or {
        text = "Choose a new name:",
        button1 = "Rename",
        button2 = "Cancel",
        hasEditBox = true,
        maxLetters = 24,
        timeout = 0,
        whileDead = true,
        hideOnEscape = true,
        OnShow = function(dialog)
            local addon = _G.MurlocMinder
            local currentName = (addon and addon:GetPet() and addon:GetPet().name) or ""
            local editBox = dialog.EditBox or dialog.editBox
            if editBox then
                editBox:SetAutoFocus(true)
                editBox:SetText(currentName or "")
                editBox:HighlightText()
            end
        end,
        OnAccept = function(dialog)
            local addon = _G.MurlocMinder
            if not addon then
                return
            end
            local editBox = dialog.EditBox or dialog.editBox
            local input = TrimString(editBox and editBox:GetText() or "")
            if input == "" then
                addon:PushActivity("Rename canceled: name cannot be empty.")
                addon:RefreshUI(true)
                return
            end
            local petData = addon:GetPet()
            local oldName = TrimString(petData.name or "")
            petData.name = input
            if oldName ~= "" and oldName ~= input then
                addon:PushActivity(string.format("%s now goes by %s.", oldName, input))
            else
                addon:PushActivity(string.format("You named %s.", input))
            end
            addon:RefreshUI(true)
            if addon.PlayUISound then
                addon:PlayUISound("popup_close")
            end
        end,
        OnCancel = function()
            local addon = _G.MurlocMinder
            if addon and addon.PlayUISound then
                addon:PlayUISound("popup_close")
            end
        end,
        EditBoxOnEnterPressed = function(editBox)
            local parent = editBox:GetParent()
            if parent and parent.button1 and parent.button1:IsEnabled() then
                parent.button1:Click()
            end
        end,
    }

    if self.PlayUISound then
        self:PlayUISound("popup_open")
    end
    StaticPopup_Show("MURLOC_MINDER_RENAME_MURLOC")
end

local function NormalizeDeleteConfirmationText(text)
    local normalized = TrimString(tostring(text or "")):lower()
    normalized = normalized:gsub("%s+", " ")
    return normalized
end

local function GetDeleteConfirmationPhrase(pet)
    local actualName = TrimString(pet and pet.name or "")
    if actualName ~= "" then
        return actualName
    end
    if pet and tostring(pet.stage or "") == "egg" then
        return "the egg"
    end
    return "unnamed murloc"
end

function MM:GetDeleteConfirmationPhrase()
    return GetDeleteConfirmationPhrase(self:GetPet())
end

function MM:ShowDeleteMurlocPrompt()
    local expectedName = self:GetDeleteConfirmationPhrase()

    StaticPopupDialogs["MURLOC_MINDER_DELETE_MURLOC"] = StaticPopupDialogs["MURLOC_MINDER_DELETE_MURLOC"] or {
        text = "Type this murloc's name to delete it:",
        button1 = "Delete",
        button2 = "Cancel",
        hasEditBox = true,
        maxLetters = 24,
        timeout = 0,
        whileDead = true,
        hideOnEscape = true,
        OnShow = function(dialog, data)
            local editBox = dialog.EditBox or dialog.editBox
            if editBox then
                editBox:SetAutoFocus(true)
                editBox:SetText("")
                editBox:HighlightText()
                editBox.expectedName = data
            end
            local text = dialog.Text or dialog.text
            if text then
                text:SetText(string.format("Type '%s' to delete this murloc:", data))
            end
        end,
        OnAccept = function(dialog)
            local editBox = dialog.EditBox or dialog.editBox
            local typed = NormalizeDeleteConfirmationText(editBox and editBox:GetText() or "")
            local expected = NormalizeDeleteConfirmationText((editBox and editBox.expectedName) or "")
            local addon = _G.MurlocMinder
            if not addon then
                return
            end
            if typed ~= expected then
                addon:PushActivity("Delete canceled: name did not match.")
                addon:RefreshUI(true)
                if addon.PlayUISound then
                    addon:PlayUISound("error")
                end
                return
            end
            if addon.PlayUISound then
                addon:PlayUISound("popup_close")
            end
            addon:ResetMurloc()
        end,
        OnCancel = function()
            local addon = _G.MurlocMinder
            if addon and addon.PlayUISound then
                addon:PlayUISound("popup_close")
            end
        end,
        EditBoxOnEnterPressed = function(editBox)
            local parent = editBox:GetParent()
            if parent and parent.button1 and parent.button1:IsEnabled() then
                parent.button1:Click()
            end
        end,
    }

    if self.PlayUISound then
        self:PlayUISound("popup_open")
    end
    if not StaticPopup_Show("MURLOC_MINDER_DELETE_MURLOC", nil, nil, expectedName) then
        self:PushActivity("Delete prompt could not be opened.")
        if self.PlayUISound then
            self:PlayUISound("error")
        end
    end
end

function MM:ShowTraitRerollConfirmPrompt(entryKey)
    local entry = self:GetStoreEntry(entryKey)
    if not entry then
        self:PushActivity("Unknown store entry.")
        return
    end

    StaticPopupDialogs["MURLOC_MINDER_CONFIRM_TRAIT_REROLL"] = StaticPopupDialogs["MURLOC_MINDER_CONFIRM_TRAIT_REROLL"] or {
        text = "",
        button1 = "Confirm",
        button2 = "Cancel",
        timeout = 0,
        whileDead = true,
        hideOnEscape = true,
        OnShow = function(dialog, data)
            local addon = _G.MurlocMinder
            local resolved = data
            if addon and data and data.entryKey then
                resolved = addon:GetStoreEntry(data.entryKey)
            end
            local text = dialog.Text or dialog.text
            if text and resolved then
                local effectivePrice = (addon and addon.GetStoreEntryEffectivePrice and addon:GetStoreEntryEffectivePrice(resolved)) or tonumber(resolved.price) or 0
                local spendText = ColorizeShellText(tostring(effectivePrice or 0) .. " shells")
                if resolved.key == "nameTag" then
                    if (effectivePrice or 0) <= 0 then
                        text:SetText(string.format("%s\n\nUse your free nametag and rename your murloc?", resolved.description or resolved.label))
                    else
                        text:SetText(string.format("%s\n\nSpend %s to use this nametag and rename your murloc?", resolved.description or resolved.label, spendText))
                    end
                else
                    text:SetText(string.format("%s\n\nSpend %s to reroll a trait?", resolved.description or resolved.label, spendText))
                end
            end
        end,
        OnAccept = function(_, data)
            local addon = _G.MurlocMinder
            if not addon or not data or not data.entryKey then
                return
            end
            local ok, msg = addon:BuyStoreEntry(data.entryKey)
            if addon.PlayUISound then
                if ok then
                    addon:PlayUISound("purchase")
                else
                    addon:PlayUISound("error")
                end
            end
            if not ok and msg then
                addon:PushActivity(msg)
            end
            addon:RefreshUI()
        end,
        OnCancel = function()
            local addon = _G.MurlocMinder
            if addon and addon.PlayUISound then
                addon:PlayUISound("popup_close")
            end
        end,
    }

    if self.PlayUISound then
        self:PlayUISound("popup_open")
    end
    StaticPopup_Show("MURLOC_MINDER_CONFIRM_TRAIT_REROLL", nil, nil, { entryKey = entryKey })
end

function MM:ShowCustomizationPurchaseConfirmPrompt(choiceKey)
    local entry = self.GetHabitatCustomizationEntry and self:GetHabitatCustomizationEntry(choiceKey)
    if not entry then
        self:PushActivity("Unknown habitat background.")
        return
    end

    StaticPopupDialogs["MURLOC_MINDER_CONFIRM_CUSTOMIZATION_PURCHASE"] = StaticPopupDialogs["MURLOC_MINDER_CONFIRM_CUSTOMIZATION_PURCHASE"] or {
        text = "",
        button1 = "Confirm",
        button2 = "Cancel",
        timeout = 0,
        whileDead = true,
        hideOnEscape = true,
        OnShow = function(dialog, data)
            local addon = _G.MurlocMinder
            local option = addon and addon.GetHabitatCustomizationEntry and addon:GetHabitatCustomizationEntry(data and data.choiceKey) or nil
            if not option then
                option = data and data.entry
            end
            local text = dialog.Text or dialog.text
            if text and option then
                local spendText = ColorizeShellText(string.format("%d shells", tonumber(option.price) or 0))
                text:SetText(string.format("Are you sure you want to buy %s for %s?", option.label or "this habitat", spendText))
            end
        end,
        OnAccept = function(_, data)
            local addon = _G.MurlocMinder
            if not addon or not data or not data.choiceKey then
                return
            end
            local ok, msg = addon:BuyHabitatBackground(data.choiceKey)
            if addon.PlayUISound then
                if ok then
                    addon:PlayUISound("purchase")
                else
                    addon:PlayUISound("error")
                end
            end
            if not ok and msg then
                addon:PushActivity(msg)
            end
            addon:RefreshUI(true)
        end,
        OnCancel = function()
            local addon = _G.MurlocMinder
            if addon and addon.PlayUISound then
                addon:PlayUISound("popup_close")
            end
        end,
    }

    if self.PlayUISound then
        self:PlayUISound("popup_open")
    end
    StaticPopup_Show("MURLOC_MINDER_CONFIRM_CUSTOMIZATION_PURCHASE", nil, nil, {
        choiceKey = choiceKey,
        entry = entry,
    })
end

function MM:UpdateDevTabVisibility()
    if not self.frame then
        return
    end

    local showDev = self.db and self.db.profile and self.db.profile.dev and self.db.profile.dev.showDevTab == true
    if self.frame.TopTabsByName and self.frame.TopTabsByName["Dev"] then
        self.frame.TopTabsByName["Dev"]:SetShown(showDev)
    end

    if not showDev and self.activeTab == "Dev" then
        self.activeTab = "Habitat"
    end
end

function MM:GetWelcomeTutorialSteps()
    local pet = self:GetPet() or {}
    local eggStage = pet.stage == "egg"
    local valueBody
    local careBody
    local adolescentSummaryBody = table.concat({
        "After hatching, your murloc enters the adolescent phase and the long-term care loop opens up.",
        "",
        string.format("- Track %s, %s, %s, and %s as your core care values.",
            ColorizeLabel("Hunger", COLOR_HUNGER),
            ColorizeLabel("Joy", COLOR_JOY),
            ColorizeLabel("Rest", COLOR_ENERGY),
            ColorizeLabel("Cleanliness", COLOR_CLEAN)
        ),
        "- Shells are your core currency and are earned from challenges and scavenging.",
        "- Spend shells in the Store on consumables, toys, trinkets, and upgrades that improve care efficiency.",
        "- I hope you get a rare one: hatch rarity can roll from Common -> Uncommon -> Rare -> Epic -> Legendary.",
        "- Traits shape behavior trends, event outcomes, and care efficiency over time.",
        "- Trait effects are passive and dynamic, so routine care still matters most.",
        "- First nametag rename is FREE in the Store (Trinkets), then later uses cost shells.",
    }, "\n")

    if eggStage then
        valueBody = "This bar tracks hatch progress. Fill it with egg care and challenge rewards until Hatch Me! is ready."
        careBody = "Use Soothe Egg, Warm Egg, and Clean Egg to nurture the hatchling. Each action has a 30s cooldown."
    else
        valueBody = "These value bars track Hunger, Joy, Energy, and Cleanliness. Keep them stable to avoid performance drops."
        careBody = "Use Feed, Play, Rest, and Clean to maintain your murloc. Item bonuses boost these same care actions."
    end

    return {
        {
            title = "Welcome to Murloc Minder",
            body = "Get ready to care for your own murloc. This quick walkthrough highlights the core panel sections you'll use most.",
            targetKey = nil,
        },
        {
            title = "Value Bars",
            body = valueBody,
            targetKey = "values",
        },
        {
            title = "Care Actions",
            body = careBody,
            targetKey = "care",
        },
        {
            title = "Quick-Buy and Shop",
            body = "Quick-Buy lets you grab commonly used items fast. Use Shop to open the full Store and manage all categories.",
            targetKey = "store",
        },
        {
            title = "Activity and Challenges",
            body = "Activity history logs what your murloc has been doing. Challenge cards provide ongoing goals and progression rewards.",
            targetKey = "boards",
        },
        {
            title = "Adolescent Phase and Traits",
            body = adolescentSummaryBody,
            targetKey = nil,
            expandedCard = true,
        },
    }
end

function MM:GetWelcomeTutorialTargetRect(targetKey)
    if not targetKey or not self.frame then
        return nil
    end

    local pet = self:GetPet() or {}
    local targets = nil
    if targetKey == "values" then
        if pet.stage == "egg" then
            targets = {
                self.frame.HatchNeedsRow,
                self.frame.HatchNeedsButton,
            }
        else
            targets = {
                self.frame.HungerRow,
                self.frame.JoyRow,
                self.frame.EnergyRow,
                self.frame.CleanRow,
            }
        end
    elseif targetKey == "care" then
        targets = {
            self.frame.FeedActionButton,
            self.frame.PlayActionButton,
            self.frame.RestActionButton,
            self.frame.CleanActionButton,
        }
    elseif targetKey == "store" then
        targets = {
            self.frame.QuickStoreFrame,
            self.frame.QuickShopButton,
        }
    elseif targetKey == "boards" then
        targets = {
            self.frame.ActivityBox,
            self.frame.ChallengePanel,
        }
    end

    local left, bottom, right, top = GetFrameRectUnion(targets)
    if not left then
        return nil
    end
    return {
        left = left,
        bottom = bottom,
        right = right,
        top = top,
    }
end

function MM:ApplyWelcomeTutorialMask(targetRect)
    local panel = self.frame and self.frame.WelcomePanel
    if not panel then
        return
    end

    local fullMask = panel.TutorialMaskFull
    local topMask = panel.TutorialMaskTop
    local bottomMask = panel.TutorialMaskBottom
    local leftMask = panel.TutorialMaskLeft
    local rightMask = panel.TutorialMaskRight
    local highlight = panel.TutorialHighlightBox
    if not fullMask or not topMask or not bottomMask or not leftMask or not rightMask then
        return
    end

    local function ShowFullMask()
        fullMask:Show()
        topMask:Hide()
        bottomMask:Hide()
        leftMask:Hide()
        rightMask:Hide()
        if highlight then
            highlight:Hide()
            if highlight.Pulse and highlight.Pulse:IsPlaying() then
                highlight.Pulse:Stop()
            end
        end
    end

    if not targetRect then
        ShowFullMask()
        return
    end

    local panelLeft, panelBottom, panelWidth, panelHeight = panel:GetRect()
    if not panelLeft or not panelBottom or not panelWidth or not panelHeight or panelWidth <= 0 or panelHeight <= 0 then
        ShowFullMask()
        return
    end

    local cutLeft = ClampNumber((targetRect.left or panelLeft) - panelLeft - TUTORIAL_TARGET_PADDING, 0, panelWidth)
    local cutRight = ClampNumber((targetRect.right or panelLeft) - panelLeft + TUTORIAL_TARGET_PADDING, 0, panelWidth)
    local cutBottom = ClampNumber((targetRect.bottom or panelBottom) - panelBottom - TUTORIAL_TARGET_PADDING, 0, panelHeight)
    local cutTop = ClampNumber((targetRect.top or panelBottom) - panelBottom + TUTORIAL_TARGET_PADDING, 0, panelHeight)

    if (cutRight - cutLeft) < 8 or (cutTop - cutBottom) < 8 then
        ShowFullMask()
        return
    end

    fullMask:Hide()

    topMask:Show()
    topMask:ClearAllPoints()
    topMask:SetPoint("TOPLEFT", panel, "TOPLEFT", 0, 0)
    topMask:SetPoint("TOPRIGHT", panel, "TOPRIGHT", 0, 0)
    topMask:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 0, cutTop)

    bottomMask:Show()
    bottomMask:ClearAllPoints()
    bottomMask:SetPoint("TOPLEFT", panel, "BOTTOMLEFT", 0, cutBottom)
    bottomMask:SetPoint("TOPRIGHT", panel, "BOTTOMRIGHT", 0, cutBottom)
    bottomMask:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 0, 0)

    leftMask:Show()
    leftMask:ClearAllPoints()
    leftMask:SetPoint("TOPLEFT", panel, "BOTTOMLEFT", 0, cutTop)
    leftMask:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 0, cutBottom)
    leftMask:SetPoint("TOPRIGHT", panel, "BOTTOMLEFT", cutLeft, cutTop)

    rightMask:Show()
    rightMask:ClearAllPoints()
    rightMask:SetPoint("TOPLEFT", panel, "BOTTOMLEFT", cutRight, cutTop)
    rightMask:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", cutRight, cutBottom)
    rightMask:SetPoint("TOPRIGHT", panel, "BOTTOMRIGHT", 0, cutTop)

    if highlight then
        highlight:Show()
        highlight:ClearAllPoints()
        highlight:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", cutLeft, cutBottom)
        highlight:SetPoint("TOPRIGHT", panel, "BOTTOMLEFT", cutRight, cutTop)
        highlight:Raise()
        if highlight.Pulse and not highlight.Pulse:IsPlaying() then
            highlight.Pulse:Play()
        end
    end
end

function MM:RefreshWelcomeTutorial()
    local panel = self.frame and self.frame.WelcomePanel
    if not panel or not panel:IsShown() then
        return
    end

    local steps = self:GetWelcomeTutorialSteps()
    local totalSteps = #steps
    if totalSteps <= 0 then
        return
    end

    local stepIndex = ClampNumber(panel._mmStepIndex or 1, 1, totalSteps)
    panel._mmStepIndex = stepIndex
    local step = steps[stepIndex]

    if panel.TutorialTitle then
        panel.TutorialTitle:SetText(step.title or "Welcome")
    end
    if panel.TutorialBody then
        panel.TutorialBody:SetText(step.body or "")
    end
    if panel.TutorialStepText then
        panel.TutorialStepText:SetText(string.format("Step %d of %d", stepIndex, totalSteps))
    end
    if panel.TutorialCard and panel.TutorialCard.SetHeight then
        panel.TutorialCard:SetHeight((step and step.expandedCard) and TUTORIAL_CARD_HEIGHT_EXPANDED or TUTORIAL_CARD_HEIGHT)
    end
    if panel.TutorialNextButton then
        panel.TutorialNextButton:SetText((stepIndex >= totalSteps) and "Finish" or "Next")
    end

    self:ApplyWelcomeTutorialMask(self:GetWelcomeTutorialTargetRect(step and step.targetKey))
end

function MM:AdvanceWelcomeTutorial()
    local panel = self.frame and self.frame.WelcomePanel
    if not panel then
        return
    end

    local steps = self:GetWelcomeTutorialSteps()
    local totalSteps = #steps
    if totalSteps <= 0 then
        self.db.profile.welcomeSeen = true
        panel:Hide()
        return
    end

    local stepIndex = ClampNumber(panel._mmStepIndex or 1, 1, totalSteps)
    if stepIndex >= totalSteps then
        self.db.profile.welcomeSeen = true
        panel:Hide()
        self:PushActivity("Tutorial complete. Keep your murloc thriving.")
        self:RefreshUI(true)
        return
    end

    panel._mmStepIndex = stepIndex + 1
    self:RefreshWelcomeTutorial()
end

function MM:InitializeUI()
    if self.frame then
        return
    end

    local frame = CreateFrame("Frame", "MurlocMinderFrame", UIParent, "PortraitFrameTemplate")
    if type(UISpecialFrames) == "table" then
        local found = false
        for _, frameName in ipairs(UISpecialFrames) do
            if frameName == "MurlocMinderFrame" then
                found = true
                break
            end
        end
        if not found then
            table.insert(UISpecialFrames, "MurlocMinderFrame")
        end
    end
    frame:SetSize(self.db.profile.frame.width, self.db.profile.frame.height)
    frame:SetPoint(self.db.profile.frame.point, UIParent, self.db.profile.frame.point, self.db.profile.frame.x, self.db.profile.frame.y)
    local initialScale = self:NormalizeMainFrameScale(self.db.profile.frame.scale)
    self.db.profile.frame.scale = initialScale
    frame:SetScale(initialScale)
    frame:SetFrameStrata("MEDIUM")
    frame:SetClampedToScreen(true)
    frame:SetMovable(true)
    frame:EnableMouse(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetScript("OnDragStart", frame.StartMoving)
    frame:SetScript("OnDragStop", function(f)
        f:StopMovingOrSizing()
        local p, _, rp, x, y = f:GetPoint(1)
        self.db.profile.frame.point = p or "CENTER"
        self.db.profile.frame.x = math.floor(x or 0)
        self.db.profile.frame.y = math.floor(y or 0)
        self.db.profile.frame.relativePoint = rp or p or "CENTER"
    end)
    frame:Hide()

    local panelTitle = string.format("Murloc Minder %s", GetDisplayVersionText())
    if frame.SetTitle then
        frame:SetTitle(panelTitle)
    elseif frame.TitleText then
        frame.TitleText:SetText(panelTitle)
    end
    if frame.SetPortraitToAsset then
        frame:SetPortraitToAsset("Interface\\Icons\\INV_Misc_Head_Murloc_01")
    end
    local portraitAnchor = frame.portrait or frame.Portrait or (frame.PortraitContainer and (frame.PortraitContainer.portrait or frame.PortraitContainer.Portrait)) or frame
    local portraitRestIcon = CreateFrame("Frame", nil, frame)
    portraitRestIcon:SetSize(24, 24)
    portraitRestIcon:SetPoint("TOPLEFT", portraitAnchor, "TOPLEFT", 2, -1)
    portraitRestIcon:SetFrameStrata(frame:GetFrameStrata())
    portraitRestIcon:SetFrameLevel(frame:GetFrameLevel() + 20)
    portraitRestIcon:Hide()

    local portraitRestTex = portraitRestIcon:CreateTexture(nil, "OVERLAY", nil, 7)
    portraitRestTex:SetAllPoints(true)
    portraitRestTex:SetTexture("Interface\\CharacterFrame\\UI-StateIcon")
    portraitRestTex:SetTexCoord(0, 0.5, 0, 0.421875)
    portraitRestTex:SetVertexColor(1, 1, 1, 1)

    local restPulse = portraitRestTex:CreateAnimationGroup()
    restPulse:SetLooping("REPEAT")
    local fadeOut = restPulse:CreateAnimation("Alpha")
    fadeOut:SetFromAlpha(1.0)
    fadeOut:SetToAlpha(0.35)
    fadeOut:SetDuration(0.8)
    fadeOut:SetOrder(1)
    local fadeIn = restPulse:CreateAnimation("Alpha")
    fadeIn:SetFromAlpha(0.35)
    fadeIn:SetToAlpha(1.0)
    fadeIn:SetDuration(0.8)
    fadeIn:SetOrder(2)
    portraitRestIcon.Pulse = restPulse

    local topNav = CreateFrame("Frame", nil, frame)
    topNav:SetSize(RIGHT_PANEL_WIDTH - 10, 52)
    topNav:SetPoint("TOPLEFT", frame, "TOPLEFT", LEFT_PANEL_WIDTH + 24, -58)

    local topPetInfoText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    topPetInfoText:SetPoint("LEFT", frame, "TOPLEFT", 54, -56)
    topPetInfoText:SetWidth(LEFT_PANEL_WIDTH - 24)
    topPetInfoText:SetJustifyH("LEFT")
    topPetInfoText:SetTextColor(1.0, 0.82, 0.1)
    topPetInfoText:SetText("")
    self:BindPetIdentityTooltip(topPetInfoText)

    local topPetMetaText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    topPetMetaText:SetPoint("TOPLEFT", topPetInfoText, "BOTTOMLEFT", 0, -2)
    topPetMetaText:SetWidth(LEFT_PANEL_WIDTH - 24)
    topPetMetaText:SetJustifyH("LEFT")
    topPetMetaText:SetText("")

    local tabNames = { "Habitat", "Store", "Customization", "Info", "Dev" }
    frame.TopTabs = {}
    frame.TopTabsByName = {}
    for i, name in ipairs(tabNames) do
        local tab = CreateTopTabButton(topNav)
        tab:SetSize(98, 24)
        local xOffset = (i - 1) * 86
        if name == "Info" or name == "Dev" then
            xOffset = xOffset + 30
        end
        tab:SetPoint("LEFT", topNav, "LEFT", xOffset, 10)
        tab:SetText(name)
        tab.selectedTextY = 1
        tab.deselectedTextY = 1
        tab._tabName = name
        tab:SetScript("OnClick", function()
            self.activeTab = name
            if name == "Store" then
                self:PushActivity("Opened full store panel.")
            end
            self:RefreshUI()
        end)
        frame.TopTabs[i] = tab
        frame.TopTabsByName[name] = tab
    end
    if self.EnsureDevInfoModule then
        local devInfoModule = self:EnsureDevInfoModule()
        if devInfoModule and devInfoModule.AttachButton then
            frame.FooterDevInfoButton = devInfoModule:AttachButton(frame, {
                point = "BOTTOMLEFT",
                relative_to = frame,
                relative_point = "BOTTOMLEFT",
                x = 12,
                y = 1,
                width = 56,
                height = 21,
                frame_level_offset = 8,
                text = "Dev Info",
            })
        end
    end
    frame.UpdateTopTabSelection = function(f, activeName)
        for _, tab in ipairs(f.TopTabs or {}) do
            local selected = tab._tabName == activeName
            if PanelTemplates_SelectTab and PanelTemplates_DeselectTab and tab.Left and tab.Middle and tab.Right and tab.Text then
                if selected then
                    PanelTemplates_SelectTab(tab)
                else
                    PanelTemplates_DeselectTab(tab)
                end
            elseif tab.SetChecked then
                tab:SetChecked(selected)
            end
            if tab.GetFontString then
                local fs = tab:GetFontString()
                if fs and fs.SetTextColor then
                    if selected then
                        fs:SetTextColor(1.0, 0.82, 0.1)
                    else
                        fs:SetTextColor(0.9, 0.85, 0.7)
                    end
                end
            end
            if selected then
                tab:LockHighlight()
            else
                tab:UnlockHighlight()
            end
        end
    end
    self:UpdateDevTabVisibility()

    local sidePanel = CreateCard(frame, LEFT_PANEL_WIDTH, 588, "TOPLEFT", frame, "TOPLEFT", 14, -88)

    local sideHeader = sidePanel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    sideHeader:SetPoint("TOPLEFT", sidePanel, "TOPLEFT", 12, -10)
    sideHeader:SetText("")
    sideHeader:Hide()

    local function CreateSectionHeader(parent, text, topY)
        local label = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        label:SetPoint("TOPLEFT", parent, "TOPLEFT", 12, topY)
        label:SetText(text)
        local divider = parent:CreateTexture(nil, "BORDER")
        divider:SetPoint("TOPLEFT", label, "BOTTOMLEFT", 0, -4)
        divider:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -12, topY - 18)
        divider:SetHeight(1)
        divider:SetColorTexture(0.22, 0.52, 0.62, 0.65)
        return label, divider
    end

    local needsTitle = CreateSectionHeader(sidePanel, "Needs", -14)
    local careTitleLabel = sidePanel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    careTitleLabel:SetPoint("TOPLEFT", sidePanel, "TOPLEFT", 12, -176)
    careTitleLabel:SetText("Care")
    local careDivider = sidePanel:CreateTexture(nil, "BORDER")
    careDivider:SetPoint("TOPLEFT", sidePanel, "TOPLEFT", 12, -194)
    careDivider:SetPoint("TOPRIGHT", sidePanel, "TOPLEFT", 148, -194)
    careDivider:SetHeight(1)
    careDivider:SetColorTexture(0.22, 0.52, 0.62, 0.65)

    local scavengingTitleLabel = sidePanel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    scavengingTitleLabel:SetPoint("TOPLEFT", sidePanel, "TOPLEFT", 166, -176)
    scavengingTitleLabel:SetText("Scavenging")
    local scavengingDivider = sidePanel:CreateTexture(nil, "BORDER")
    scavengingDivider:SetPoint("TOPLEFT", sidePanel, "TOPLEFT", 166, -194)
    scavengingDivider:SetPoint("TOPRIGHT", sidePanel, "TOPRIGHT", -12, -194)
    scavengingDivider:SetHeight(1)
    scavengingDivider:SetColorTexture(0.22, 0.52, 0.62, 0.65)

    local careScavengeDivider = sidePanel:CreateTexture(nil, "BORDER")
    careScavengeDivider:SetPoint("TOPLEFT", sidePanel, "TOPLEFT", 156, -198)
    careScavengeDivider:SetPoint("BOTTOMLEFT", sidePanel, "TOPLEFT", 156, -360)
    careScavengeDivider:SetWidth(1)
    careScavengeDivider:SetColorTexture(0.22, 0.52, 0.62, 0.65)

    local actionButtons = {
        { key = "FeedAction", text = "Feed" },
        { key = "PlayAction", text = "Play" },
        { key = "CleanAction", text = "Clean" },
        { key = "RestAction", text = "Rest" },
    }

    local previous
    for _, data in ipairs(actionButtons) do
        local button = CreateFrame("Button", nil, sidePanel, "UIPanelButtonTemplate")
        button:SetSize(136, 24)
        if previous then
            button:SetPoint("TOPLEFT", previous, "BOTTOMLEFT", 0, -7)
        else
            button:SetPoint("TOPLEFT", sidePanel, "TOPLEFT", 12, -198)
        end
        button:SetText(data.text)
        SkinButton(button)
        SkinSetListButton(button)
        sidePanel[data.key] = button
        previous = button
    end

    local workActionButton = CreateFrame("Button", nil, sidePanel, "UIPanelButtonTemplate")
    workActionButton:SetSize(136, 24)
    workActionButton:SetPoint("TOPLEFT", sidePanel, "TOPLEFT", 166, -198)
    workActionButton:SetText("Send To Scavenge")
    SkinButton(workActionButton)
    SkinSetListButton(workActionButton)
    sidePanel.WorkAction = workActionButton

    sidePanel.FeedAction:SetScript("OnClick", function()
        if self.PlayCareActionButtonSound then
            self:PlayCareActionButtonSound("feed")
        end
        local pet = self:GetPet()
        local ok, msg
        if pet.stage == "egg" then
            ok, msg = false, "The egg cannot be fed."
        else
            ok, msg = self:FeedFood("__free__")
        end
        if not ok and msg then
            self:PushActivity(msg)
        end
        self:RefreshUI()
    end)
    sidePanel.PlayAction:SetScript("OnClick", function()
        if self.PlayCareActionButtonSound then
            self:PlayCareActionButtonSound("play")
        end
        local ok, msg = self:PlayWithMurloc()
        if ok == false and msg then
            self:PushActivity(msg)
            self:RefreshUI()
        end
    end)
    sidePanel.CleanAction:SetScript("OnClick", function()
        if self.PlayCareActionButtonSound then
            self:PlayCareActionButtonSound("clean")
        end
        local ok, msg = self:CleanHabitat()
        if ok == false and msg then
            self:PushActivity(msg)
            self:RefreshUI()
        end
    end)
    sidePanel.RestAction:SetScript("OnClick", function()
        if self.PlayCareActionButtonSound then
            self:PlayCareActionButtonSound("rest")
        end
        local ok, msg = self:RestMurloc()
        if ok == false and msg then
            self:PushActivity(msg)
            self:RefreshUI()
        end
    end)
    sidePanel.WorkAction:SetScript("OnClick", function()
        self:ToggleWorkShift()
    end)

    local careAccordion = CreateFrame("Frame", nil, sidePanel)
    careAccordion:SetPoint("TOPLEFT", sidePanel, "TOPLEFT", 12, -198)
    careAccordion:SetSize(136, 188)
    careAccordion.sections = {}
    careAccordion.order = { "feed", "play", "rest", "clean" }

    local function createCareSection(sectionKey, label)
        local section = {}
        section.key = sectionKey
        section.label = label
        section.header = CreateFrame("Button", nil, careAccordion, "UIPanelButtonTemplate")
        section.header:SetSize(136, 24)
        section.header._mmSectionKey = sectionKey
        SkinButton(section.header)
        SkinSetListButton(section.header)
        if section.header._selectedTexture then
            section.header._selectedTexture:SetVertexColor(1.0, 0.85, 0.15, 1.0)
            section.header._selectedTexture:SetAlpha(0.42)
        end
        -- Keep tab color state-driven only (selected vs unselected), not hover-driven.
        if section.header.LeftHighlight then section.header.LeftHighlight:SetAlpha(0) end
        if section.header.MiddleHighlight then section.header.MiddleHighlight:SetAlpha(0) end
        if section.header.RightHighlight then section.header.RightHighlight:SetAlpha(0) end
        section.header:SetScript("OnClick", function()
            self.db.profile.ui = self.db.profile.ui or {}
            self.db.profile.ui.openCareSection = sectionKey
            self:RefreshUI()
        end)
        section.cooldownBar = CreateLinearCooldownBar(section.header)

        section.rows = {}
        for i = 1, 8 do
            local row = CreateFrame("Button", nil, careAccordion, "UIPanelButtonTemplate")
            row:SetSize(143, 20)
            row:SetNormalFontObject("GameFontNormalTiny")
            row:SetHighlightFontObject("GameFontHighlightSmall")
            row:SetDisabledFontObject("GameFontDisableSmall")
            SkinButton(row)
            row:SetText("")

            local ownedText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalTiny")
            ownedText:SetPoint("LEFT", row, "LEFT", 4, 0)
            ownedText:SetWidth(18)
            ownedText:SetJustifyH("LEFT")

            local nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalTiny")
            nameText:SetPoint("LEFT", row, "LEFT", 22, 0)
            nameText:SetPoint("RIGHT", row, "RIGHT", -36, 0)
            nameText:SetJustifyH("LEFT")
            if nameText.SetWordWrap then
                nameText:SetWordWrap(false)
            end
            if nameText.SetMaxLines then
                nameText:SetMaxLines(1)
            end

            local effectText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalTiny")
            effectText:SetPoint("RIGHT", row, "RIGHT", -4, 0)
            effectText:SetJustifyH("RIGHT")
            if effectText.SetWordWrap then
                effectText:SetWordWrap(false)
            end
            if effectText.SetMaxLines then
                effectText:SetMaxLines(1)
            end

            row.ownedText = ownedText
            row.nameText = nameText
            row.effectText = effectText
            row:SetScript("OnClick", function(button)
                if not button.itemKey then
                    return
                end

                if self.PlayCareActionButtonSound then
                    self:PlayCareActionButtonSound(sectionKey)
                end
                local ok, msg
                if sectionKey == "feed" then
                    ok, msg = self:FeedFood(button.itemIsFree and "__free__" or button.itemKey)
                elseif sectionKey == "play" then
                    ok, msg = self:PlayWithMurloc(button.itemIsFree and "__free__" or button.itemKey)
                elseif sectionKey == "rest" then
                    ok, msg = self:RestMurloc(button.itemIsFree and "__free__" or button.itemKey)
                elseif sectionKey == "clean" then
                    ok, msg = self:CleanHabitat(button.itemIsFree and "__free__" or button.itemKey)
                end

                if ok == false and msg then
                    self:PushActivity(msg)
                    self:RefreshUI()
                end
            end)
            row:SetScript("OnEnter", function(button)
                if not button.itemKey then
                    return
                end
                local tooltipSource = "store"
                if sectionKey == "feed" then
                    tooltipSource = "food"
                end
                if button.itemIsFree then
                    tooltipSource = "basic"
                end
                self:ShowStoreItemTooltip(button, tooltipSource, button.itemKey, sectionKey)
            end)
            row:SetScript("OnLeave", function()
                GameTooltip:Hide()
            end)
            row:Hide()
            section.rows[i] = row
        end

        careAccordion.sections[sectionKey] = section
    end

    createCareSection("feed", "Feed")
    createCareSection("play", "Play")
    createCareSection("rest", "Rest")
    createCareSection("clean", "Clean")

    local sootheCooldownBar = CreateLinearCooldownBar(sidePanel.PlayAction)
    local cleanCooldownBar = CreateLinearCooldownBar(sidePanel.CleanAction)
    local warmCooldownBar = CreateLinearCooldownBar(sidePanel.RestAction)

    local identity = sidePanel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    identity:SetPoint("TOPLEFT", sidePanel, "TOPLEFT", 156, -40)
    identity:SetWidth(164)
    identity:SetJustifyH("LEFT")
    identity:SetText("Unhatched Egg")
    identity:Hide()

    local details = sidePanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    details:SetPoint("TOPLEFT", identity, "BOTTOMLEFT", 0, -8)
    details:SetWidth(170)
    details:SetJustifyH("LEFT")
    details:SetText("Stage: Egg")
    details:Hide()

    local workStatus = sidePanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    workStatus:SetPoint("TOPLEFT", sidePanel, "TOPLEFT", 166, -198)
    workStatus:SetWidth(174)
    workStatus:SetJustifyH("LEFT")

    local economySummary = sidePanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    economySummary:SetPoint("TOPLEFT", sidePanel, "TOPLEFT", 166, -252)
    economySummary:SetWidth(164)
    economySummary:SetJustifyH("LEFT")

    local statsHeader = sidePanel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    statsHeader:SetPoint("TOPLEFT", sidePanel, "TOPLEFT", 12, -14)
    statsHeader:SetText("Needs")
    statsHeader:Hide()

    local hungerRow = BuildStatusBar(sidePanel, "Hunger", -38, { 0.38, 0.92, 0.57 })
    local joyRow = BuildStatusBar(sidePanel, "Joy", -72, { 0.35, 0.72, 1.0 })
    local energyRow = BuildStatusBar(sidePanel, "Energy", -106, { 0.97, 0.8, 0.31 })
    local cleanRow = BuildStatusBar(sidePanel, "Cleanliness", -140, { 0.76, 0.58, 0.95 })
    local hatchNeedsRow = BuildStatusBar(sidePanel, "Hatch Progress", -72, COLOR_HATCH)
    local hatchNeedsButton = CreateFrame("Button", nil, sidePanel, "UIPanelButtonTemplate")
    hatchNeedsButton:SetSize(255, 22)
    hatchNeedsButton:SetPoint("TOPLEFT", hatchNeedsRow, "BOTTOMLEFT", 0, -6)
    hatchNeedsButton:SetText("Hatch Me!")
    SkinButton(hatchNeedsButton)
    hatchNeedsButton:SetScript("OnClick", function()
        local ok, msg = self:HatchEggNow()
        if not ok and msg then
            self:PushActivity(msg)
        end
        self:RefreshUI()
    end)

    local storeFrame = CreateCard(sidePanel, LEFT_PANEL_WIDTH - 16, 196, "BOTTOMLEFT", sidePanel, "BOTTOMLEFT", 8, 8)

    local storeHeader = storeFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    storeHeader:SetPoint("TOPLEFT", storeFrame, "TOPLEFT", 10, -10)
    storeHeader:SetText("Quick-Buy")

    local shellsText = storeFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    shellsText:SetPoint("TOPRIGHT", storeFrame, "TOPRIGHT", -10, -11)

    local storeHeaderDivider = storeFrame:CreateTexture(nil, "BORDER")
    storeHeaderDivider:SetPoint("TOPLEFT", storeFrame, "TOPLEFT", 10, -28)
    storeHeaderDivider:SetPoint("TOPRIGHT", storeFrame, "TOPRIGHT", -10, -28)
    storeHeaderDivider:SetHeight(1)
    storeHeaderDivider:SetColorTexture(0.22, 0.52, 0.62, 0.65)

    local quickBuyEntries = {
        { key = "kelpSnack", source = "food" },
        { key = "stickFigure", source = "store" },
        { key = "shellPillow", source = "store" },
        { key = "bubbleSponge", source = "store" },
    }

    storeFrame.rows = {}
    for index, quick in ipairs(quickBuyEntries) do
        local item
        if quick.source == "food" then
            for _, food in ipairs(self:GetFoodItems()) do
                if food.key == quick.key then
                    item = food
                    break
                end
            end
        else
            item = self:GetStoreEntry(quick.key)
        end
        if item then
            local row = CreateQuickBuyRow(storeFrame, item, index)
            row.itemSource = quick.source
            row.itemKey = quick.key
            if row.name then
                local itemColor = GetNeedColorByItemSourceAndKey(self, quick.source, quick.key)
                row.name:SetText(ColorizeLabel(item.label, itemColor))
            end
            row.buy:SetScript("OnClick", function()
                if self.PlayUISound then
                    self:PlayUISound("button")
                end
                local ok, msg
                if quick.source == "food" then
                    ok, msg = self:BuyFood(quick.key)
                else
                    if IsConfirmUseStoreEntryKey(quick.key) then
                        self:ShowTraitRerollConfirmPrompt(quick.key)
                        return
                    end
                    ok, msg = self:BuyStoreEntry(quick.key)
                end
                if self.PlayUISound then
                    if ok then
                        self:PlayUISound("purchase")
                    else
                        self:PlayUISound("error")
                    end
                end
                if not ok and msg then
                    self:PushActivity(msg)
                end
                self:RefreshUI()
            end)
            row:SetScript("OnEnter", function(owner)
                self:ShowStoreItemTooltip(owner, quick.source, quick.key, quick.source == "food" and "feed" or nil)
            end)
            row:SetScript("OnLeave", function()
                GameTooltip:Hide()
            end)
            storeFrame.rows[quick.key] = row
        end
    end

    local quickShopButton = CreateFrame("Button", nil, storeFrame, "UIPanelButtonTemplate")
    quickShopButton:SetSize(math.max(120, storeFrame:GetWidth() - 24), 56)
    quickShopButton:SetPoint("BOTTOM", storeFrame, "BOTTOM", 0, 8)
    quickShopButton:SetText("Shop")
    SkinButton(quickShopButton)
    quickShopButton:SetNormalFontObject("GameFontHighlightLarge")
    quickShopButton:SetHighlightFontObject("GameFontHighlightLarge")
    quickShopButton:SetScript("OnClick", function()
        self.activeTab = "Store"
        self:RefreshUI(true)
    end)

    local quickShopIcon = quickShopButton:CreateTexture(nil, "ARTWORK")
    quickShopIcon:SetSize(16, 16)
    quickShopIcon:SetPoint("LEFT", quickShopButton, "LEFT", 10, 0)
    quickShopIcon:SetTexture("Interface\\Icons\\INV_Misc_Coin_01")
    quickShopIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    local habitat = CreateCard(frame, RIGHT_PANEL_WIDTH, 588, "TOPRIGHT", frame, "TOPRIGHT", -12, -88)

    local splitDivider = frame:CreateTexture(nil, "BORDER")
    splitDivider:SetPoint("TOPLEFT", sidePanel, "TOPRIGHT", 3, 0)
    splitDivider:SetPoint("BOTTOMLEFT", sidePanel, "BOTTOMRIGHT", 3, 0)
    splitDivider:SetWidth(1)
    splitDivider:SetColorTexture(0.32, 0.32, 0.32, 0.5)

    local habitatTitle = habitat:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    habitatTitle:SetPoint("TOPLEFT", habitat, "TOPLEFT", 12, -10)
    habitatTitle:SetText("")
    habitatTitle:Hide()

    local habitatBG = habitat:CreateTexture(nil, "ARTWORK")
    habitatBG:SetPoint("TOPLEFT", habitat, "TOPLEFT", 8, -8)
    habitatBG:SetPoint("BOTTOMRIGHT", habitat, "BOTTOMRIGHT", -8, 122)
    if not habitatBG:SetAtlas("shop-bg-environment-unavailable-murloc", true) then
        habitatBG:SetTexture("Interface\\Shop\\CatalogShopUnavailableMurloc")
        habitatBG:SetTexCoord(0, 1, 0, 1)
    end

    local habitatModelBG = CreateFrame("Model", nil, habitat)
    habitatModelBG:SetPoint("TOPLEFT", habitat, "TOPLEFT", 8, -8)
    habitatModelBG:SetPoint("BOTTOMRIGHT", habitat, "BOTTOMRIGHT", -8, 122)
    habitatModelBG:SetFrameStrata("MEDIUM")
    habitatModelBG:SetFrameLevel(habitat:GetFrameLevel() + 1)
    habitatModelBG:Hide()

    local habitatShade = habitat:CreateTexture(nil, "BORDER")
    habitatShade:SetPoint("TOPLEFT", habitatBG, "TOPLEFT", 0, 0)
    habitatShade:SetPoint("BOTTOMRIGHT", habitatBG, "BOTTOMRIGHT", 0, 0)
    habitatShade:SetColorTexture(0.01, 0.01, 0.01, 0.24)

    local petTexture = habitat:CreateTexture(nil, "ARTWORK")
    petTexture:SetSize(230, 230)
    petTexture:SetPoint("CENTER", habitatBG, "CENTER", -90, -8)
    petTexture:SetTexture(ICON_EGG)
    petTexture:SetDrawLayer("OVERLAY", 5)
    petTexture:SetAlpha(1)
    self:BindPetIdentityTooltip(petTexture)

    local eggModel = CreateFrame("PlayerModel", nil, habitat)
    eggModel:SetAllPoints(habitatBG)
    eggModel:SetFrameStrata(frame:GetFrameStrata())
    eggModel:SetFrameLevel(habitat:GetFrameLevel() + 8)
    eggModel:Hide()
    self:BindPetIdentityTooltip(eggModel)

    local habitatModelInput = CreateFrame("Frame", nil, habitat)
    habitatModelInput:SetPoint("TOPLEFT", habitatBG, "TOPLEFT", 0, 0)
    habitatModelInput:SetPoint("BOTTOMRIGHT", habitatBG, "BOTTOMRIGHT", 0, 0)
    habitatModelInput:SetFrameStrata(frame:GetFrameStrata())
    habitatModelInput:SetFrameLevel(habitat:GetFrameLevel() + 12)
    habitatModelInput:EnableMouse(MODEL_CONTROLS_ENABLED)
    habitatModelInput:EnableMouseWheel(MODEL_CONTROLS_ENABLED)
    habitatModelInput:SetScript("OnMouseDown", function(inputFrame, button)
        if self.activeTab ~= "Habitat" or not self.frame or not self.frame.EggModel or not self.frame.EggModel:IsShown() then
            return
        end
        if button ~= "LeftButton" and button ~= "RightButton" then
            return
        end
        local stageKey = self:GetModelStageKey()
        local state = self:GetModelControlState(stageKey)
        inputFrame._mmDragStageKey = stageKey
        inputFrame._mmDragMode = (button == "RightButton") and "spin" or "move"
        inputFrame._mmDragStartX, inputFrame._mmDragStartY = GetScaledCursorPosition()
        inputFrame._mmStartModelX = state.x
        inputFrame._mmStartModelY = state.y
        inputFrame._mmStartModelZ = state.z
        inputFrame._mmStartModelFacing = state.facing
        inputFrame._mmDragging = true
        if stageKey == "egg" then
            self:StopEggWiggle(true)
        end
    end)
    habitatModelInput:SetScript("OnMouseUp", function(inputFrame)
        inputFrame._mmDragging = false
        inputFrame._mmDragMode = nil
    end)
    habitatModelInput:SetScript("OnLeave", function(inputFrame)
        if not inputFrame._mmDragging then
            return
        end
        if not IsMouseButtonDown("LeftButton") and not IsMouseButtonDown("RightButton") then
            inputFrame._mmDragging = false
            inputFrame._mmDragMode = nil
        end
    end)
    habitatModelInput:SetScript("OnUpdate", function(inputFrame)
        if not inputFrame._mmDragging then
            return
        end
        if self.activeTab ~= "Habitat" or not self.frame or not self.frame.EggModel or not self.frame.EggModel:IsShown() then
            inputFrame._mmDragging = false
            inputFrame._mmDragMode = nil
            return
        end
        if inputFrame._mmDragMode == "move" and (not IsMouseButtonDown("LeftButton")) then
            inputFrame._mmDragging = false
            inputFrame._mmDragMode = nil
            return
        end
        if inputFrame._mmDragMode == "spin" and (not IsMouseButtonDown("RightButton")) then
            inputFrame._mmDragging = false
            inputFrame._mmDragMode = nil
            return
        end

        local stageKey = inputFrame._mmDragStageKey or self:GetModelStageKey()
        local state = self:GetModelControlState(stageKey)
        local cursorX, cursorY = GetScaledCursorPosition()
        local startX = inputFrame._mmDragStartX or cursorX
        local startY = inputFrame._mmDragStartY or cursorY
        local dx = cursorX - startX
        local dy = cursorY - startY

        if inputFrame._mmDragMode == "move" then
            local dragCoeff = self:GetDepthDragCoefficient(stageKey, inputFrame._mmStartModelX or state.x)
            local moveStep = MODEL_MOVE_PER_PIXEL * dragCoeff
            if MODEL_UNCLAMP_DRAG then
                state.y = self:Clamp((inputFrame._mmStartModelY or state.y) + (dx * moveStep), MODEL_POS_Y_MIN, MODEL_POS_Y_MAX)
                state.z = self:Clamp((inputFrame._mmStartModelZ or state.z) + (dy * moveStep), MODEL_POS_Z_MIN, MODEL_POS_Z_MAX)
            else
                local yMin, yMax, zMin, zMax = self:GetScaleAwareModelBounds(stageKey, state.scale, state.cam)
                state.y = self:Clamp((inputFrame._mmStartModelY or state.y) + (dx * moveStep), yMin, yMax)
                state.z = self:Clamp((inputFrame._mmStartModelZ or state.z) + (dy * moveStep), zMin, zMax)
            end
        elseif inputFrame._mmDragMode == "spin" then
            state.facing = (inputFrame._mmStartModelFacing or state.facing) + (dx * MODEL_SPIN_PER_PIXEL)
        end

        self:ApplyModelControlState(stageKey)
    end)
    habitatModelInput:SetScript("OnMouseWheel", function(_, delta)
        if self.activeTab ~= "Habitat" or not self.frame or not self.frame.EggModel or not self.frame.EggModel:IsShown() then
            return
        end
        local stageKey = self:GetModelStageKey()
        local state = self:GetModelControlState(stageKey)
        state.x = self:Clamp((state.x or 0) + ((delta or 0) * MODEL_DEPTH_STEP), MODEL_POS_X_MIN, MODEL_POS_X_MAX)
        self:ApplyModelControlState(stageKey)
        if stageKey == "egg" then
            self:StopEggWiggle(true)
        end
    end)

    local resetModelButton = CreateFrame("Button", nil, habitat, "UIPanelButtonTemplate")
    resetModelButton:SetSize(24, 20)
    resetModelButton:SetPoint("TOPRIGHT", habitatBG, "TOPRIGHT", -8, -8)
    resetModelButton:SetFrameStrata(frame:GetFrameStrata())
    resetModelButton:SetFrameLevel(habitat:GetFrameLevel() + 14)
    SkinButton(resetModelButton)
    resetModelButton:SetText("")
    if resetModelButton.Left then resetModelButton.Left:SetVertexColor(0.86, 0.2, 0.2) end
    if resetModelButton.Middle then resetModelButton.Middle:SetVertexColor(0.86, 0.2, 0.2) end
    if resetModelButton.Right then resetModelButton.Right:SetVertexColor(0.86, 0.2, 0.2) end
    local resetModelIcon = resetModelButton:CreateTexture(nil, "ARTWORK")
    resetModelIcon:SetPoint("CENTER", resetModelButton, "CENTER", 0, 0)
    resetModelIcon:SetSize(14, 14)
    if not resetModelIcon:SetAtlas("dashboard-panel-homestone-teleport-button", false) then
        resetModelIcon:SetTexture("Interface\\Icons\\INV_Misc_Horn_02")
        resetModelIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    end
    resetModelIcon:SetVertexColor(1.0, 0.84, 0.18)
    resetModelButton:SetScript("OnClick", function()
        self:ResetCurrentModelControlState()
    end)
    resetModelButton:SetScript("OnEnter", function(button)
        GameTooltip:SetOwner(button, "ANCHOR_LEFT")
        GameTooltip:AddLine(string.format("reset %s's position", self:GetPetDisplayName()), 1.0, 0.82, 0.0, true)
        GameTooltip:Show()
    end)
    resetModelButton:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    if not MODEL_CONTROLS_ENABLED then
        resetModelButton:Hide()
    end

    local bottomRowWidth = RIGHT_PANEL_WIDTH - 20
    local bottomPaneGap = 8
    local activityPaneWidth = math.floor((bottomRowWidth - bottomPaneGap) / 2)
    local challengePaneWidth = bottomRowWidth - activityPaneWidth - bottomPaneGap

    local challengePanel = CreateCard(habitat, challengePaneWidth, 112, "BOTTOMRIGHT", habitat, "BOTTOMRIGHT", -10, 10)
    challengePanel:SetFrameStrata(frame:GetFrameStrata())
    challengePanel:SetFrameLevel(habitat:GetFrameLevel() + 80)
    challengePanel:SetToplevel(true)
    challengePanel:SetClipsChildren(true)

    local challengeHeaderDivider = challengePanel:CreateTexture(nil, "BORDER")
    challengeHeaderDivider:SetPoint("TOPLEFT", challengePanel, "TOPLEFT", 10, -8)
    challengeHeaderDivider:SetPoint("TOPRIGHT", challengePanel, "TOPRIGHT", -10, -8)
    challengeHeaderDivider:SetHeight(1)
    challengeHeaderDivider:SetColorTexture(0.22, 0.52, 0.62, 0.65)

    local challengeCells = {}
    local cellGap = 6
    local cellWidth = math.floor((challengePaneWidth - 20 - cellGap) / 2)
    local cellHeight = math.floor((112 - 24 - cellGap) / 2)
    for i = 1, 4 do
        local cell = CreateFrame("Frame", nil, challengePanel)
        cell:SetFrameStrata(challengePanel:GetFrameStrata())
        cell:SetFrameLevel(challengePanel:GetFrameLevel() + 1)
        cell:EnableMouse(true)
        local col = (i - 1) % 2
        local row = math.floor((i - 1) / 2)
        cell:SetSize(cellWidth, cellHeight - 1)
        cell:SetPoint("TOPLEFT", challengePanel, "TOPLEFT", 10 + (col * (cellWidth + cellGap)), -12 - (row * (cellHeight + cellGap)))

        local bg = cell:CreateTexture(nil, "BACKGROUND")
        bg:SetAllPoints(true)
        bg:SetColorTexture(0.08, 0.13, 0.18, 0.45)

        local divider = cell:CreateTexture(nil, "BORDER")
        divider:SetPoint("BOTTOMLEFT", cell, "BOTTOMLEFT", 0, 0)
        divider:SetPoint("BOTTOMRIGHT", cell, "BOTTOMRIGHT", 0, 0)
        divider:SetHeight(1)
        divider:SetColorTexture(0.16, 0.58, 0.72, 0.55)

        local text = cell:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        text:SetPoint("TOPLEFT", cell, "TOPLEFT", 4, -4)
        text:SetPoint("BOTTOMRIGHT", cell, "BOTTOMRIGHT", -4, 4)
        text:SetJustifyH("LEFT")
        text:SetJustifyV("TOP")
        text:SetText("")

        local reloadButton = CreateFrame("Button", nil, cell, "UIPanelCloseButton")
        reloadButton:SetSize(20, 20)
        reloadButton:SetPoint("TOPRIGHT", cell, "TOPRIGHT", 3, 3)
        reloadButton:SetFrameStrata(challengePanel:GetFrameStrata())
        reloadButton:SetFrameLevel(challengePanel:GetFrameLevel() + 10)
        reloadButton:SetScale(0.75)
        local reloadCooldownText = cell:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        reloadCooldownText:SetPoint("TOPRIGHT", reloadButton, "BOTTOMRIGHT", 1, -1)
        reloadCooldownText:SetJustifyH("RIGHT")
        reloadCooldownText:SetText("")
        reloadCooldownText:Hide()
        local cardIndex = i
        reloadButton:SetScript("OnClick", function()
            if not self.ReloadChallengeAtIndex then
                return
            end
            local ok, msg = self:ReloadChallengeAtIndex(cardIndex)
            if not ok and msg then
                self:PushActivity(msg)
                self:RefreshUI(true)
            end
        end)
        reloadButton:SetScript("OnEnter", function(button)
            GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
            local reloadCost = 20
            local remaining = (self.GetChallengeReloadRemaining and self:GetChallengeReloadRemaining(cardIndex)) or 0
            local eggRerollRemaining = (self.GetChallengeRerollRemaining and self:GetChallengeRerollRemaining()) or 0
            local pet = self:GetPet()
            local store = self:GetStore()
            local canAfford = (store.shells or 0) >= reloadCost
            GameTooltip:AddLine("Reload Challenge", 1.0, 0.82, 0.0)
            GameTooltip:AddLine("Replace this card with a new challenge.", 0.9, 0.9, 0.9, true)
            GameTooltip:AddLine(" ")
            if pet and pet.stage == "egg" then
                GameTooltip:AddLine("Cost: Free (egg stage)", 0.75, 0.9, 1.0, true)
                if eggRerollRemaining > 0 then
                    local mins = math.floor(eggRerollRemaining / 60)
                    local secs = math.floor(eggRerollRemaining % 60)
                    GameTooltip:AddLine(string.format("Cooldown: %d:%02d remaining", mins, secs), 1.0, 0.2, 0.2)
                else
                    GameTooltip:AddLine("Cooldown: Ready", 0.2, 1.0, 0.2)
                end
            elseif remaining > 0 then
                GameTooltip:AddLine(string.format("Cost: %d shells", reloadCost), COLOR_SHELL[1], COLOR_SHELL[2], COLOR_SHELL[3])
                GameTooltip:AddLine(string.format("Cooldown: %ds remaining", math.ceil(remaining)), 1.0, 0.2, 0.2)
            else
                GameTooltip:AddLine(string.format("Cost: %d shells", reloadCost), COLOR_SHELL[1], COLOR_SHELL[2], COLOR_SHELL[3])
                GameTooltip:AddLine("Cooldown: Ready", 0.2, 1.0, 0.2)
            end
            if (not (pet and pet.stage == "egg")) and not canAfford then
                GameTooltip:AddLine(string.format("Need %d more shells.", reloadCost - (store.shells or 0)), COLOR_SHELL[1], COLOR_SHELL[2], COLOR_SHELL[3])
            end
            GameTooltip:Show()
        end)
        reloadButton:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)
        cell:SetScript("OnEnter", function(owner)
            self:ShowChallengeTooltip(cardIndex, owner)
        end)
        cell:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)

        local flash = cell:CreateTexture(nil, "ARTWORK")
        flash:SetAllPoints(true)
        flash:SetColorTexture(1, 1, 1, 0)
        local glow = CreateOutlinePulseFrame(cell, 10, 3)

        challengeCells[i] = {
            index = i,
            frame = cell,
            text = text,
            reloadButton = reloadButton,
            reloadCooldownText = reloadCooldownText,
            flash = flash,
            glow = glow,
            _elapsed = 0,
            _lastRenderKey = nil,
            _activeChallengeID = nil,
            _displayText = "",
            _transition = nil,
        }

        cell:SetScript("OnUpdate", function(_, elapsed)
            if not self or not self.frame or not self.frame:IsShown() then
                return
            end
            self:TickChallengeCard(cardIndex, elapsed)
        end)
    end

    local storeDetailPanel = CreateCard(habitat, RIGHT_PANEL_WIDTH - 16, 458, "TOPLEFT", habitat, "TOPLEFT", 8, -28)
    storeDetailPanel:SetFrameLevel(habitat:GetFrameLevel() + 25)
    storeDetailPanel:Hide()

    local storeDetailTitle = storeDetailPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    storeDetailTitle:SetPoint("TOPLEFT", storeDetailPanel, "TOPLEFT", 10, -10)
    storeDetailTitle:SetText("")
    storeDetailTitle:Hide()

    local storeDetailShells = storeDetailPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    storeDetailShells:SetPoint("TOPRIGHT", storeDetailPanel, "TOPRIGHT", -10, -8)
    storeDetailShells:SetJustifyH("RIGHT")
    storeDetailShells:SetText(ColorizeShellText("Shells: 0"))

    local storeDetailTotals = storeDetailPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    storeDetailTotals:SetPoint("TOPLEFT", storeDetailPanel, "TOPLEFT", 232, -24)
    storeDetailTotals:SetPoint("TOPRIGHT", storeDetailPanel, "TOPRIGHT", -26, -24)
    storeDetailTotals:SetJustifyH("LEFT")
    storeDetailTotals:SetJustifyV("TOP")
    storeDetailTotals:SetText("")

    local sectionDropdown
    do
        local ok, modernDropdown = pcall(CreateFrame, "DropdownButton", "MurlocMinderStoreSectionDropdown", storeDetailPanel, "WowStyle1DropdownTemplate")
        if ok and modernDropdown and modernDropdown.SetupMenu then
            sectionDropdown = modernDropdown
            sectionDropdown:SetPoint("TOPLEFT", storeDetailPanel, "TOPLEFT", 10, -10)
            sectionDropdown:SetWidth(210)
            sectionDropdown:SetDefaultText("Select Section")
            sectionDropdown:SetSelectionTranslator(function(selection)
                return selection and selection.data and selection.data.label or ""
            end)
            sectionDropdown:SetupMenu(function(_, rootDescription)
                rootDescription:SetTag("MENU_MURLOC_MINDER_STORE_SECTIONS")
                local sections = self:GetStoreSections()
                local function IsSelected(section)
                    local store = self:GetStore()
                    local selected = store.selectedSection
                    if selected == "feeding" then
                        selected = "habitat"
                    end
                    return (selected or "food") == section.key
                end
                local function SetSelected(section)
                    local store = self:GetStore()
                    store.selectedSection = section.key
                    store.storeScrollOffsets = store.storeScrollOffsets or {}
                    store.storeScrollOffsets[section.key] = 0
                    self:RefreshUI()
                end
                for _, section in ipairs(sections) do
                    rootDescription:CreateRadio(section.label, IsSelected, SetSelected, section)
                end
            end)
        else
            sectionDropdown = CreateFrame("Frame", "MurlocMinderStoreSectionDropdown", storeDetailPanel, "UIDropDownMenuTemplate")
            sectionDropdown:SetPoint("TOPLEFT", storeDetailPanel, "TOPLEFT", -6, -6)
            UIDropDownMenu_SetWidth(sectionDropdown, 210)
            UIDropDownMenu_JustifyText(sectionDropdown, "LEFT")
            UIDropDownMenu_Initialize(sectionDropdown, function(_, _, _)
                local sections = self:GetStoreSections()
                for _, section in ipairs(sections) do
                    local info = UIDropDownMenu_CreateInfo()
                    info.text = section.label
                    info.value = section.key
                    info.func = function()
                        local store = self:GetStore()
                        store.selectedSection = section.key
                        store.storeScrollOffsets = store.storeScrollOffsets or {}
                        store.storeScrollOffsets[section.key] = 0
                        UIDropDownMenu_SetSelectedValue(sectionDropdown, section.key)
                        UIDropDownMenu_SetText(sectionDropdown, section.label)
                        self:RefreshUI()
                    end
                    info.checked = function()
                        local store = self:GetStore()
                        local selected = store.selectedSection
                        if selected == "feeding" then
                            selected = "habitat"
                        end
                        return (selected or "food") == section.key
                    end
                    UIDropDownMenu_AddButton(info)
                end
            end)
        end
    end

    local storeDetailRows = {}
    local storeDetailRowOrder = {}
    local rowIndex = 1
    for _, entry in ipairs(self:GetStoreCatalog()) do
        local row = CreateStoreDetailRow(storeDetailPanel, rowIndex)
        row.Buy:SetScript("OnClick", function()
            if self.PlayUISound then
                self:PlayUISound("button")
            end
            if IsConfirmUseStoreEntryKey(entry.key) then
                self:ShowTraitRerollConfirmPrompt(entry.key)
                return
            end
            local ok, msg = self:BuyStoreEntry(entry.key)
            if self.PlayUISound then
                if ok then
                    self:PlayUISound("purchase")
                else
                    self:PlayUISound("error")
                end
            end
            if not ok and msg then
                self:PushActivity(msg)
            end
            self:RefreshUI()
        end)
        row._entryKey = entry.key
        row._entrySource = "store"
        row:SetScript("OnEnter", function(owner)
            self:ShowStoreItemTooltip(owner, "store", row._entryKey)
        end)
        row:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)
        storeDetailRows[entry.key] = row
        storeDetailRowOrder[#storeDetailRowOrder + 1] = row
        rowIndex = rowIndex + 1
    end
    for _, food in ipairs(self:GetFoodItems()) do
        local row = CreateStoreDetailRow(storeDetailPanel, rowIndex)
        row.Buy:SetScript("OnClick", function()
            if self.PlayUISound then
                self:PlayUISound("button")
            end
            local ok, msg = self:BuyFood(food.key)
            if self.PlayUISound then
                if ok then
                    self:PlayUISound("purchase")
                else
                    self:PlayUISound("error")
                end
            end
            if not ok and msg then
                self:PushActivity(msg)
            end
            self:RefreshUI()
        end)
        row._entryKey = food.key
        row._entrySource = "food"
        row:SetScript("OnEnter", function(owner)
            self:ShowStoreItemTooltip(owner, "food", row._entryKey, "feed")
        end)
        row:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)
        storeDetailRows["food:" .. food.key] = row
        storeDetailRowOrder[#storeDetailRowOrder + 1] = row
        rowIndex = rowIndex + 1
    end

    local storeDetailScroll
    do
        local okModern, modern = pcall(CreateFrame, "Slider", nil, storeDetailPanel, "WowTrimScrollBarTemplate")
        if okModern and modern then
            storeDetailScroll = modern
        else
            storeDetailScroll = CreateFrame("Slider", nil, storeDetailPanel, "UIPanelScrollBarTemplate")
        end
    end
    storeDetailScroll:SetPoint("TOPRIGHT", storeDetailPanel, "TOPRIGHT", -8, -66)
    storeDetailScroll:SetPoint("BOTTOMRIGHT", storeDetailPanel, "BOTTOMRIGHT", -8, 90)
    storeDetailScroll:SetWidth(18)
    if storeDetailScroll.SetOrientation then
        storeDetailScroll:SetOrientation("VERTICAL")
    end
    storeDetailScroll:SetMinMaxValues(0, 0)
    storeDetailScroll:SetValueStep(1)
    if storeDetailScroll.SetObeyStepOnDrag then
        storeDetailScroll:SetObeyStepOnDrag(true)
    end
    storeDetailScroll:SetScript("OnValueChanged", function() end)
    storeDetailScroll:SetValue(0)
    if storeDetailScroll.Low then storeDetailScroll.Low:Hide() end
    if storeDetailScroll.High then storeDetailScroll.High:Hide() end
    if storeDetailScroll.Text then storeDetailScroll.Text:Hide() end
    storeDetailScroll:Hide()

    storeDetailPanel:EnableMouseWheel(true)
    storeDetailPanel:SetScript("OnMouseWheel", function(_, delta)
        local store = self:GetStore()
        local selectedSection = store.selectedSection
        if selectedSection == "feeding" then
            selectedSection = "habitat"
        end
        selectedSection = selectedSection or "food"
        store.storeScrollOffsets = store.storeScrollOffsets or {}

        local current = tonumber(store.storeScrollOffsets[selectedSection]) or 0
        local maxOffset = tonumber(storeDetailScroll._maxOffset) or 0
        if maxOffset <= 0 then
            return
        end

        local nextOffset = self:Clamp(current - delta, 0, maxOffset)
        nextOffset = math.floor(nextOffset + 0.5)
        if nextOffset ~= current then
            store.storeScrollOffsets[selectedSection] = nextOffset
            storeDetailScroll:SetValue(nextOffset)
            self:RefreshUI()
        end
    end)

    storeDetailScroll:SetScript("OnValueChanged", function(_, value)
        local store = self:GetStore()
        local selectedSection = store.selectedSection
        if selectedSection == "feeding" then
            selectedSection = "habitat"
        end
        selectedSection = selectedSection or "food"
        store.storeScrollOffsets = store.storeScrollOffsets or {}
        local maxOffset = tonumber(storeDetailScroll._maxOffset) or 0
        local nextOffset = self:Clamp(math.floor((value or 0) + 0.5), 0, maxOffset)
        if (tonumber(store.storeScrollOffsets[selectedSection]) or 0) ~= nextOffset then
            store.storeScrollOffsets[selectedSection] = nextOffset
            self:RefreshUI()
        end
    end)

    local customizationPanel = CreateCard(habitat, RIGHT_PANEL_WIDTH - 16, 458, "TOPLEFT", habitat, "TOPLEFT", 8, -28)
    customizationPanel:SetFrameLevel(habitat:GetFrameLevel() + 24)
    customizationPanel:Hide()

    local customizationShellsText = customizationPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    customizationShellsText:SetPoint("TOPRIGHT", customizationPanel, "TOPRIGHT", -10, -8)
    customizationShellsText:SetJustifyH("RIGHT")
    customizationShellsText:SetText(ColorizeShellText("Shells: 0"))

    local customizationPageDropdown
    do
        local ok, modernDropdown = pcall(CreateFrame, "DropdownButton", "MurlocMinderCustomizationPageDropdown", customizationPanel, "WowStyle1DropdownTemplate")
        if ok and modernDropdown and modernDropdown.SetupMenu then
            customizationPageDropdown = modernDropdown
            customizationPageDropdown:SetPoint("TOPLEFT", customizationPanel, "TOPLEFT", 10, -10)
            customizationPageDropdown:SetWidth(210)
            customizationPageDropdown:SetDefaultText("Select Page")
            customizationPageDropdown:SetupMenu(function(_, rootDescription)
                rootDescription:SetTag("MENU_MURLOC_MINDER_CUSTOMIZATION_PAGES")
                local pages = {
                    { key = "habitats", label = "Habitats" },
                }
                local function IsSelected(page)
                    local customization = self.GetHabitatCustomizationState and self:GetHabitatCustomizationState() or {}
                    return tostring(customization.selectedPage or "habitats") == page.key
                end
                local function SetSelected(page)
                    local customization = self.GetHabitatCustomizationState and self:GetHabitatCustomizationState() or {}
                    customization.selectedPage = page.key
                    self:RefreshUI(true)
                end
                for _, page in ipairs(pages) do
                    rootDescription:CreateRadio(page.label, IsSelected, SetSelected, page)
                end
            end)
        else
            customizationPageDropdown = CreateFrame("Frame", "MurlocMinderCustomizationPageDropdown", customizationPanel, "UIDropDownMenuTemplate")
            customizationPageDropdown:SetPoint("TOPLEFT", customizationPanel, "TOPLEFT", -6, -6)
            UIDropDownMenu_SetWidth(customizationPageDropdown, 210)
            UIDropDownMenu_JustifyText(customizationPageDropdown, "LEFT")
            UIDropDownMenu_Initialize(customizationPageDropdown, function(_, _, _)
                local info = UIDropDownMenu_CreateInfo()
                info.text = "Habitats"
                info.value = "habitats"
                info.func = function()
                    local customization = self.GetHabitatCustomizationState and self:GetHabitatCustomizationState() or {}
                    customization.selectedPage = "habitats"
                    UIDropDownMenu_SetSelectedValue(customizationPageDropdown, "habitats")
                    UIDropDownMenu_SetText(customizationPageDropdown, "Habitats")
                    self:RefreshUI(true)
                end
                info.checked = function()
                    local customization = self.GetHabitatCustomizationState and self:GetHabitatCustomizationState() or {}
                    return tostring(customization.selectedPage or "habitats") == "habitats"
                end
                UIDropDownMenu_AddButton(info)
            end)
        end
    end

    local customizationHabitatCards = {}
    local habitatOptions = self.GetHabitatCustomizationCatalog and self:GetHabitatCustomizationCatalog() or {}
    for i, option in ipairs(habitatOptions) do
        local card = CreateHabitatCustomizationCard(customizationPanel, i)
        card._mmChoiceKey = option.key
        ApplyHabitatBackgroundPreviewByChoice(card.Preview, option.key)

        card.Buy:SetScript("OnClick", function()
            if self.PlayUISound then
                self:PlayUISound("button")
            end
            local owned = self.IsHabitatBackgroundOwned and self:IsHabitatBackgroundOwned(option.key)
            if owned then
                local okApply, applyMsg = self:BuyHabitatBackground(option.key)
                if self.PlayUISound then
                    if okApply then
                        self:PlayUISound("purchase")
                    else
                        self:PlayUISound("error")
                    end
                end
                if not okApply and applyMsg then
                    self:PushActivity(applyMsg)
                end
                self:RefreshUI(true)
                return
            end
            self:ShowCustomizationPurchaseConfirmPrompt(option.key)
        end)

        card:SetScript("OnEnter", function(owner)
            GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
            GameTooltip:AddLine(option.label or "Habitat", 1.0, 0.82, 0.0, true)
            local price = tonumber(option.price) or 0
            if price <= 0 then
                GameTooltip:AddLine(ColorizeLabel("Price: FREE", COLOR_HATCH), 1, 1, 1, true)
            else
                GameTooltip:AddLine("Price: " .. ColorizeShellText(string.format("%d shells", price)), 1, 1, 1, true)
            end
            if option.description and option.description ~= "" then
                GameTooltip:AddLine(option.description, 0.9, 0.9, 0.9, true)
            end
            GameTooltip:Show()
        end)
        card:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)

        customizationHabitatCards[#customizationHabitatCards + 1] = card
    end

    local tabPlaceholderPanel = CreateCard(habitat, RIGHT_PANEL_WIDTH - 16, 458, "TOPLEFT", habitat, "TOPLEFT", 8, -28)
    tabPlaceholderPanel:SetFrameLevel(habitat:GetFrameLevel() + 24)
    tabPlaceholderPanel:Hide()

    local tabPlaceholderTitle = tabPlaceholderPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    tabPlaceholderTitle:SetPoint("TOPLEFT", tabPlaceholderPanel, "TOPLEFT", 14, -18)
    tabPlaceholderTitle:SetText("Coming Soon")

    local tabPlaceholderBody = tabPlaceholderPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    tabPlaceholderBody:SetPoint("TOPLEFT", tabPlaceholderTitle, "BOTTOMLEFT", 0, -14)
    tabPlaceholderBody:SetWidth(RIGHT_PANEL_WIDTH - 50)
    tabPlaceholderBody:SetJustifyH("LEFT")
    tabPlaceholderBody:SetText("This section is planned for a future update.")

    local carePanel = CreateCard(habitat, RIGHT_PANEL_WIDTH - 16, 458, "TOPLEFT", habitat, "TOPLEFT", 8, -28)
    carePanel:SetFrameLevel(habitat:GetFrameLevel() + 24)
    carePanel:Hide()

    local careTitle = carePanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    careTitle:SetPoint("TOPLEFT", carePanel, "TOPLEFT", 14, -18)
    careTitle:SetText("")
    careTitle:Hide()

    local careSummaryScroll = CreateFrame("ScrollFrame", nil, carePanel, "UIPanelScrollFrameTemplate")
    careSummaryScroll:SetPoint("TOPLEFT", carePanel, "TOPLEFT", 14, -14)
    careSummaryScroll:SetPoint("BOTTOMLEFT", carePanel, "BOTTOMLEFT", 14, 70)
    careSummaryScroll:SetWidth(312)
    careSummaryScroll:EnableMouseWheel(true)

    local careSummaryContent = CreateFrame("Frame", nil, careSummaryScroll)
    careSummaryContent:SetSize(300, 1)
    careSummaryScroll:SetScrollChild(careSummaryContent)

    local careBody = careSummaryContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    careBody:SetPoint("TOPLEFT", careSummaryContent, "TOPLEFT", 0, 0)
    careBody:SetWidth(300)
    careBody:SetJustifyH("LEFT")
    careBody:SetJustifyV("TOP")
    careBody:SetText("Loading murloc information...")

    careSummaryScroll:SetScript("OnMouseWheel", function(scrollFrame, delta)
        local nextValue = (scrollFrame:GetVerticalScroll() or 0) - ((delta or 0) * 28)
        if nextValue < 0 then
            nextValue = 0
        end
        scrollFrame:SetVerticalScroll(nextValue)
    end)

    local infoTelemetryButton = CreateFrame("Button", nil, carePanel, "UIPanelButtonTemplate")
    infoTelemetryButton:SetSize(86, 28)
    infoTelemetryButton:SetPoint("BOTTOMLEFT", carePanel, "BOTTOMLEFT", 14, 30)
    infoTelemetryButton:SetText("Telemetry")
    SkinButton(infoTelemetryButton)
    infoTelemetryButton:SetScript("OnClick", function()
        if self.ShowTelemetryReport then
            self:ShowTelemetryReport(false)
        end
    end)

    local renameButton = CreateFrame("Button", nil, carePanel, "UIPanelButtonTemplate")
    renameButton:SetSize(136, 28)
    renameButton:SetPoint("LEFT", infoTelemetryButton, "RIGHT", 8, 0)
    renameButton:SetText("Rename Murloc")
    SkinButton(renameButton)
    renameButton:SetScript("OnClick", function()
        self:ShowRenameMurlocPrompt()
    end)

    local infoPurchasesToggleButton = CreateFrame("Button", nil, carePanel, "UIPanelButtonTemplate")
    infoPurchasesToggleButton:SetSize(96, 24)
    infoPurchasesToggleButton:SetPoint("BOTTOMLEFT", infoTelemetryButton, "TOPLEFT", 0, 8)
    infoPurchasesToggleButton:SetText("Purchases +")
    SkinButton(infoPurchasesToggleButton)
    infoPurchasesToggleButton:SetScript("OnClick", function()
        self.db.profile.ui = self.db.profile.ui or {}
        self.db.profile.ui.infoExpandPurchases = not (self.db.profile.ui.infoExpandPurchases == true)
        self:RefreshUI(true)
    end)

    local infoLossesToggleButton = CreateFrame("Button", nil, carePanel, "UIPanelButtonTemplate")
    infoLossesToggleButton:SetSize(88, 24)
    infoLossesToggleButton:SetPoint("LEFT", infoPurchasesToggleButton, "RIGHT", 8, 0)
    infoLossesToggleButton:SetText("Losses +")
    SkinButton(infoLossesToggleButton)
    infoLossesToggleButton:SetScript("OnClick", function()
        self.db.profile.ui = self.db.profile.ui or {}
        self.db.profile.ui.infoExpandLosses = not (self.db.profile.ui.infoExpandLosses == true)
        self:RefreshUI(true)
    end)

    careSummaryScroll:ClearAllPoints()
    careSummaryScroll:SetPoint("TOPLEFT", carePanel, "TOPLEFT", 14, -14)
    careSummaryScroll:SetPoint("BOTTOMLEFT", infoPurchasesToggleButton, "TOPLEFT", 0, 6)

    local infoActivityPanel = CreateCard(carePanel, 388, 418, "TOPRIGHT", carePanel, "TOPRIGHT", -10, -10)
    local infoActivityTitle = infoActivityPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    infoActivityTitle:SetPoint("TOPLEFT", infoActivityPanel, "TOPLEFT", 10, -10)
    infoActivityTitle:SetText("Activity History")

    local infoActivityText = CreateFrame("ScrollingMessageFrame", nil, infoActivityPanel)
    infoActivityText:SetPoint("TOPLEFT", infoActivityPanel, "TOPLEFT", 10, -30)
    infoActivityText:SetPoint("BOTTOMRIGHT", infoActivityPanel, "BOTTOMRIGHT", -10, 10)
    infoActivityText:SetFontObject(GameFontNormalSmall)
    infoActivityText:SetJustifyH("LEFT")
    infoActivityText:SetFading(false)
    infoActivityText:SetMaxLines(5000)
    infoActivityText:SetInsertMode("BOTTOM")
    infoActivityText:EnableMouseWheel(true)
    infoActivityText:SetScript("OnMouseWheel", function(self, delta)
        if delta > 0 then
            self:ScrollUp()
        else
            self:ScrollDown()
        end
    end)

    local devPanel = CreateCard(habitat, RIGHT_PANEL_WIDTH - 16, 458, "TOPLEFT", habitat, "TOPLEFT", 8, -28)
    devPanel:SetFrameLevel(habitat:GetFrameLevel() + 24)
    devPanel:Hide()

    local devTitle = devPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    devTitle:SetPoint("TOPLEFT", devPanel, "TOPLEFT", 14, -18)
    devTitle:SetText("")
    devTitle:Hide()

    local devBody = devPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    devBody:SetPoint("TOPLEFT", devPanel, "TOPLEFT", 14, -14)
    devBody:SetWidth(RIGHT_PANEL_WIDTH - 60)
    devBody:SetJustifyH("LEFT")
    devBody:SetText("Testing overrides (not for normal gameplay).")

    local devDeleteButton = CreateFrame("Button", nil, devPanel, "UIPanelButtonTemplate")
    devDeleteButton:SetSize(230, 28)
    devDeleteButton:SetPoint("TOPLEFT", devBody, "BOTTOMLEFT", 0, -20)
    devDeleteButton:SetText("Force Delete Current Murloc")
    SkinButton(devDeleteButton)
    devDeleteButton:SetScript("OnClick", function()
        self:ResetMurloc()
    end)

    local devEggReadyButton = CreateFrame("Button", nil, devPanel, "UIPanelButtonTemplate")
    devEggReadyButton:SetSize(230, 28)
    devEggReadyButton:SetPoint("TOPLEFT", devDeleteButton, "BOTTOMLEFT", 0, -10)
    devEggReadyButton:SetText("Set Egg To 100%")
    SkinButton(devEggReadyButton)
    devEggReadyButton:SetScript("OnClick", function()
        local pet = self:GetPet()
        pet.stage = "egg"
        pet.hatchProgress = 100
        pet.readyToHatch = true
        pet.eggCare = pet.eggCare or {
            careProgress = 0,
            lastWarmAt = 0,
            lastCleanAt = 0,
            lastSootheAt = 0,
        }
        self:PushActivity("Dev: egg set to hatch-ready.")
        self:RefreshUI(true)
    end)

    local devMaxNeedsButton = CreateFrame("Button", nil, devPanel, "UIPanelButtonTemplate")
    devMaxNeedsButton:SetSize(230, 28)
    devMaxNeedsButton:SetPoint("TOPLEFT", devEggReadyButton, "BOTTOMLEFT", 0, -10)
    devMaxNeedsButton:SetText("Max All Needs")
    SkinButton(devMaxNeedsButton)
    devMaxNeedsButton:SetScript("OnClick", function()
        local pet = self:GetPet()
        pet.hunger = 100
        pet.joy = 100
        pet.energy = 100
        pet.cleanliness = 100
        self:PushActivity("Dev: needs set to 100%.")
        self:RefreshUI(true)
    end)

    local devAddShellsButton = CreateFrame("Button", nil, devPanel, "UIPanelButtonTemplate")
    devAddShellsButton:SetSize(230, 28)
    devAddShellsButton:SetPoint("TOPLEFT", devMaxNeedsButton, "BOTTOMLEFT", 0, -10)
    devAddShellsButton:SetText("+100 Shells")
    SkinButton(devAddShellsButton)
    devAddShellsButton:SetScript("OnClick", function()
        if self.AddShells then
            self:AddShells(100)
        else
            local store = self:GetStore()
            local economy = self:GetEconomy()
            store.shells = (tonumber(store.shells) or 0) + 100
            economy.shellsEarned = (tonumber(economy.shellsEarned) or 0) + 100
        end
        self:PushActivity("Dev: added 100 shells.")
        self:RefreshUI(true)
    end)

    local devClearCooldownsButton = CreateFrame("Button", nil, devPanel, "UIPanelButtonTemplate")
    devClearCooldownsButton:SetSize(230, 28)
    devClearCooldownsButton:SetPoint("TOPLEFT", devAddShellsButton, "BOTTOMLEFT", 0, -10)
    devClearCooldownsButton:SetText("Clear Cooldowns")
    SkinButton(devClearCooldownsButton)
    devClearCooldownsButton:SetScript("OnClick", function()
        local pet = self:GetPet()
        local economy = self:GetEconomy()
        pet.actionCooldowns = pet.actionCooldowns or {}
        pet.actionCooldowns.feed = 0
        pet.actionCooldowns.play = 0
        pet.actionCooldowns.clean = 0
        pet.actionCooldowns.rest = 0
        pet.actionCooldowns.find = 0
        pet.eggCare = pet.eggCare or {}
        pet.eggCare.lastWarmAt = 0
        pet.eggCare.lastCleanAt = 0
        pet.eggCare.lastSootheAt = 0
        economy.challenges = economy.challenges or {}
        economy.challenges.nextRerollAt = 0
        economy.challenges.reloadReadyAt = 0
        economy.work = economy.work or {}
        economy.work.nextStartAllowedAt = 0
        self:PushActivity("Dev: cooldowns cleared.")
        self:RefreshUI(true)
    end)

    local devResetFirstUseButton = CreateFrame("Button", nil, devPanel, "UIPanelButtonTemplate")
    devResetFirstUseButton:SetSize(230, 28)
    devResetFirstUseButton:SetPoint("TOPLEFT", devClearCooldownsButton, "BOTTOMLEFT", 0, -10)
    devResetFirstUseButton:SetText("Reset To First Use")
    SkinButton(devResetFirstUseButton)
    devResetFirstUseButton:SetScript("OnClick", function()
        self:ResetProfile()
        self:InitializePetState(true)
        self.db.profile.welcomeSeen = false
        local economy = self:GetEconomy()
        economy.challenges = economy.challenges or {}
        economy.challenges.eggTutorialShown = false
        economy.challenges.nextRerollAt = 0
        economy.challenges.reloadReadyAt = 0
        if self.frame and self.frame.WelcomePanel then
            self.frame.WelcomePanel._mmStepIndex = 1
        end
        self.activeTab = "Habitat"
        self:ResetMainFramePosition()
        self:UpdateDevTabVisibility()
        self:RefreshUI(true)
    end)

    local devRenameLabel = devPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    devRenameLabel:SetPoint("TOPLEFT", devResetFirstUseButton, "BOTTOMLEFT", 0, -12)
    devRenameLabel:SetText("Rename Murloc")

    local devRenameEditBox = CreateFrame("EditBox", nil, devPanel, "InputBoxTemplate")
    devRenameEditBox:SetSize(170, 24)
    devRenameEditBox:SetPoint("TOPLEFT", devRenameLabel, "BOTTOMLEFT", 0, -6)
    devRenameEditBox:SetAutoFocus(false)
    devRenameEditBox:SetMaxLetters(24)

    local devRenameApplyButton = CreateFrame("Button", nil, devPanel, "UIPanelButtonTemplate")
    devRenameApplyButton:SetSize(54, 24)
    devRenameApplyButton:SetPoint("LEFT", devRenameEditBox, "RIGHT", 6, 0)
    devRenameApplyButton:SetText("Apply")
    SkinButton(devRenameApplyButton)
    devRenameApplyButton:SetScript("OnClick", function()
        local pet = self:GetPet()
        if not pet or pet.stage == "egg" then
            self:PushActivity("Dev: cannot rename during egg stage.")
            return
        end
        local input = TrimString(devRenameEditBox:GetText() or "")
        if input == "" then
            self:PushActivity("Dev: rename canceled (empty name).")
            return
        end
        local oldName = TrimString(pet.name or "")
        pet.name = input
        if oldName ~= "" and oldName ~= input then
            self:PushActivity(string.format("Dev: renamed %s to %s.", oldName, input))
        else
            self:PushActivity(string.format("Dev: name set to %s.", input))
        end
        self:RefreshUI(true)
    end)
    devRenameEditBox:SetScript("OnEnterPressed", function()
        devRenameApplyButton:Click()
    end)

    local devMurlocSoundsLabel = devPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    devMurlocSoundsLabel:SetPoint("TOPLEFT", devRenameEditBox, "BOTTOMLEFT", 0, -8)
    devMurlocSoundsLabel:SetText("Murloc Sounds")

    local devMurlocSoundsCard = CreateCard(devPanel, 230, DEV_MURLOC_SOUND_LIST_HEIGHT, "TOPLEFT", devMurlocSoundsLabel, "BOTTOMLEFT", 0, -4)
    local devMurlocSoundsCount = devPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    devMurlocSoundsCount:SetPoint("BOTTOMRIGHT", devMurlocSoundsCard, "TOPRIGHT", -8, 2)
    devMurlocSoundsCount:SetText("")

    local devMurlocSoundsEmpty = devMurlocSoundsCard:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    devMurlocSoundsEmpty:SetPoint("CENTER", devMurlocSoundsCard, "CENTER", 0, 0)
    devMurlocSoundsEmpty:SetText("No murloc sounds found")
    devMurlocSoundsEmpty:Hide()

    local devMurlocSoundsScroll = CreateFrame("ScrollFrame", nil, devMurlocSoundsCard, "FauxScrollFrameTemplate")
    devMurlocSoundsScroll:SetPoint("TOPLEFT", devMurlocSoundsCard, "TOPLEFT", 4, -4)
    devMurlocSoundsScroll:SetPoint("BOTTOMRIGHT", devMurlocSoundsCard, "BOTTOMRIGHT", -26, 4)
    devMurlocSoundsScroll:SetScript("OnVerticalScroll", function(scrollFrame, offset)
        if FauxScrollFrame_OnVerticalScroll then
            FauxScrollFrame_OnVerticalScroll(scrollFrame, offset, DEV_MURLOC_SOUND_ROW_HEIGHT, function()
                self:RefreshUI()
            end)
        end
    end)
    devMurlocSoundsCard:EnableMouseWheel(true)
    devMurlocSoundsCard:SetScript("OnMouseWheel", function(_, delta)
        if not FauxScrollFrame_GetOffset or not FauxScrollFrame_SetOffset then
            return
        end
        local sounds = GetDevMurlocSoundEntries()
        local maxOffset = math.max(0, #sounds - DEV_MURLOC_SOUND_VISIBLE_ROWS)
        local currentOffset = FauxScrollFrame_GetOffset(devMurlocSoundsScroll) or 0
        local nextOffset = currentOffset + (delta < 0 and 1 or -1)
        if nextOffset < 0 then
            nextOffset = 0
        elseif nextOffset > maxOffset then
            nextOffset = maxOffset
        end
        if nextOffset ~= currentOffset then
            FauxScrollFrame_SetOffset(devMurlocSoundsScroll, nextOffset)
            self:RefreshUI()
        end
    end)

    local devMurlocSoundRows = {}
    for i = 1, DEV_MURLOC_SOUND_VISIBLE_ROWS do
        local row = CreateFrame("Frame", nil, devMurlocSoundsCard)
        row:SetPoint("TOPLEFT", devMurlocSoundsCard, "TOPLEFT", 6, -6 - ((i - 1) * DEV_MURLOC_SOUND_ROW_HEIGHT))
        row:SetPoint("TOPRIGHT", devMurlocSoundsCard, "TOPRIGHT", -30, -6 - ((i - 1) * DEV_MURLOC_SOUND_ROW_HEIGHT))
        row:SetHeight(DEV_MURLOC_SOUND_ROW_HEIGHT)

        local indexText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        indexText:SetPoint("LEFT", row, "LEFT", 2, 0)
        indexText:SetWidth(26)
        indexText:SetJustifyH("RIGHT")

        local labelText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        labelText:SetPoint("LEFT", indexText, "RIGHT", 4, 0)
        labelText:SetPoint("RIGHT", row, "RIGHT", -46, 0)
        labelText:SetJustifyH("LEFT")
        labelText:SetWordWrap(false)

        local playButton = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
        playButton:SetSize(42, 18)
        playButton:SetPoint("RIGHT", row, "RIGHT", -2, 0)
        playButton:SetText("Play")
        SkinButton(playButton)
        playButton:SetScript("OnClick", function(button)
            local soundEntry = button._mmSoundEntry
            if not soundEntry then
                return
            end
            local played = false
            if self.PlayDevPreviewSound then
                local previewPath = (soundEntry.mode == "file") and soundEntry.key or nil
                played = self:PlayDevPreviewSound(soundEntry.id, soundEntry.mode, previewPath)
            end
            local label = tostring(soundEntry.display or soundEntry.key or ("Sound " .. tostring(soundEntry.id)))
            if self.Print then
                if played then
                    self:Print(string.format("Murloc sound %d: %s (%d)", soundEntry.index, label, soundEntry.id))
                else
                    self:Print(string.format("Could not play murloc sound %d: %s (%d)", soundEntry.index, label, soundEntry.id))
                end
            end
        end)

        row.IndexText = indexText
        row.LabelText = labelText
        row.PlayButton = playButton
        row:Hide()
        devMurlocSoundRows[#devMurlocSoundRows + 1] = row
    end

    local DEV_COL2_X = 260
    local DEV_COL3_X = 430
    local DEV_COL2_WIDTH = 170
    local DEV_COL3_WIDTH = 245

    local devPersonalityLabel = devPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    devPersonalityLabel:SetPoint("TOPLEFT", devBody, "TOPLEFT", DEV_COL2_X, -6)
    devPersonalityLabel:SetText("Traits")

    local devPersonalityDropdown
    local devSecondaryPersonalityDropdown
    do
        local okPrimary, modernPrimary = pcall(CreateFrame, "DropdownButton", "MurlocMinderDevPersonalityDropdown", devPanel, "WowStyle1DropdownTemplate")
        if okPrimary and modernPrimary and modernPrimary.SetupMenu then
            devPersonalityDropdown = modernPrimary
            devPersonalityDropdown:SetPoint("TOPLEFT", devPersonalityLabel, "BOTTOMLEFT", 0, -2)
            devPersonalityDropdown:SetWidth(DEV_COL2_WIDTH)
            devPersonalityDropdown:SetDefaultText("Primary Trait")
            devPersonalityDropdown:SetupMenu(function(_, rootDescription)
                rootDescription:SetTag("MENU_MURLOC_MINDER_DEV_PRIMARY_TRAIT")
                local personalities = self.GetPersonalities and self:GetPersonalities() or {}
                local function IsSelected(personality)
                    self.db.profile.dev = self.db.profile.dev or {}
                    local pet = self:GetPet()
                    return (self.db.profile.dev.selectedPersonality or pet.personality) == personality
                end
                local function SetSelected(personality)
                    self.db.profile.dev = self.db.profile.dev or {}
                    self.db.profile.dev.selectedPersonality = personality
                    if devPersonalityDropdown.OverrideText then
                        devPersonalityDropdown:OverrideText(personality)
                    end
                end
                for _, personality in ipairs(personalities) do
                    rootDescription:CreateRadio("Primary: " .. personality, IsSelected, SetSelected, personality)
                end
            end)

            local okSecondary, modernSecondary = pcall(CreateFrame, "DropdownButton", "MurlocMinderDevSecondaryPersonalityDropdown", devPanel, "WowStyle1DropdownTemplate")
            if okSecondary and modernSecondary and modernSecondary.SetupMenu then
                devSecondaryPersonalityDropdown = modernSecondary
                devSecondaryPersonalityDropdown:SetPoint("TOPLEFT", devPersonalityDropdown, "BOTTOMLEFT", 0, -8)
                devSecondaryPersonalityDropdown:SetWidth(DEV_COL2_WIDTH)
                devSecondaryPersonalityDropdown:SetDefaultText("Secondary Trait")
                devSecondaryPersonalityDropdown:SetupMenu(function(_, rootDescription)
                    rootDescription:SetTag("MENU_MURLOC_MINDER_DEV_SECONDARY_TRAIT")
                    local personalities = self.GetPersonalities and self:GetPersonalities() or {}
                    local function IsSelected(personality)
                        self.db.profile.dev = self.db.profile.dev or {}
                        local pet = self:GetPet()
                        return (self.db.profile.dev.selectedSecondaryPersonality or pet.personalitySecondary) == personality
                    end
                    local function SetSelected(personality)
                        self.db.profile.dev = self.db.profile.dev or {}
                        self.db.profile.dev.selectedSecondaryPersonality = personality
                        if devSecondaryPersonalityDropdown.OverrideText then
                            devSecondaryPersonalityDropdown:OverrideText(personality)
                        end
                    end
                    for _, personality in ipairs(personalities) do
                        rootDescription:CreateRadio("Secondary: " .. personality, IsSelected, SetSelected, personality)
                    end
                end)
            end
        end

        if not devPersonalityDropdown then
            devPersonalityDropdown = CreateFrame("Frame", "MurlocMinderDevPersonalityDropdown", devPanel, "UIDropDownMenuTemplate")
            devPersonalityDropdown:SetPoint("TOPLEFT", devPersonalityLabel, "BOTTOMLEFT", -14, -2)
            UIDropDownMenu_SetWidth(devPersonalityDropdown, DEV_COL2_WIDTH)
            UIDropDownMenu_JustifyText(devPersonalityDropdown, "LEFT")
            UIDropDownMenu_Initialize(devPersonalityDropdown, function(_, _, _)
                local personalities = self.GetPersonalities and self:GetPersonalities() or {}
                for _, personality in ipairs(personalities) do
                    local info = UIDropDownMenu_CreateInfo()
                    info.text = "Primary: " .. personality
                    info.value = personality
                    info.func = function()
                        self.db.profile.dev = self.db.profile.dev or {}
                        self.db.profile.dev.selectedPersonality = personality
                        UIDropDownMenu_SetSelectedValue(devPersonalityDropdown, personality)
                        UIDropDownMenu_SetText(devPersonalityDropdown, personality)
                    end
                    UIDropDownMenu_AddButton(info)
                end
            end)
        end

        if not devSecondaryPersonalityDropdown then
            devSecondaryPersonalityDropdown = CreateFrame("Frame", "MurlocMinderDevSecondaryPersonalityDropdown", devPanel, "UIDropDownMenuTemplate")
            devSecondaryPersonalityDropdown:SetPoint("TOPLEFT", devPersonalityDropdown, "BOTTOMLEFT", 0, -8)
            UIDropDownMenu_SetWidth(devSecondaryPersonalityDropdown, DEV_COL2_WIDTH)
            UIDropDownMenu_JustifyText(devSecondaryPersonalityDropdown, "LEFT")
            UIDropDownMenu_Initialize(devSecondaryPersonalityDropdown, function(_, _, _)
                local personalities = self.GetPersonalities and self:GetPersonalities() or {}
                for _, personality in ipairs(personalities) do
                    local info = UIDropDownMenu_CreateInfo()
                    info.text = "Secondary: " .. personality
                    info.value = personality
                    info.func = function()
                        self.db.profile.dev = self.db.profile.dev or {}
                        self.db.profile.dev.selectedSecondaryPersonality = personality
                        UIDropDownMenu_SetSelectedValue(devSecondaryPersonalityDropdown, personality)
                        UIDropDownMenu_SetText(devSecondaryPersonalityDropdown, personality)
                    end
                    UIDropDownMenu_AddButton(info)
                end
            end)
        end
    end

    local devApplyPersonalityButton = CreateFrame("Button", nil, devPanel, "UIPanelButtonTemplate")
    devApplyPersonalityButton:SetSize(DEV_COL2_WIDTH, 22)
    devApplyPersonalityButton:SetPoint("TOPLEFT", devSecondaryPersonalityDropdown, "BOTTOMLEFT", 0, -2)
    devApplyPersonalityButton:SetText("Apply Traits")
    SkinButton(devApplyPersonalityButton)
    devApplyPersonalityButton:SetScript("OnClick", function()
        local pet = self:GetPet()
        self.db.profile.dev = self.db.profile.dev or {}
        local chosenPrimary = self.db.profile.dev.selectedPersonality or pet.personality
        local chosenSecondary = self.db.profile.dev.selectedSecondaryPersonality or pet.personalitySecondary
        if chosenPrimary and chosenPrimary ~= "" then
            local personalities = self.GetPersonalities and self:GetPersonalities() or {}
            if not chosenSecondary or chosenSecondary == "" or chosenSecondary == chosenPrimary then
                for _, candidate in ipairs(personalities) do
                    if candidate ~= chosenPrimary then
                        chosenSecondary = candidate
                        break
                    end
                end
            end
            pet.personality = chosenPrimary
            pet.personalitySecondary = chosenSecondary
            self:PushActivity("Dev: traits set to " .. self:GetTraitDisplayText(pet) .. ".")
            if self.GetDirectionalHatchMessage then
                self:PushActivity(self:GetDirectionalHatchMessage(pet))
            end
            self:RefreshUI(true)
        end
    end)

    local devModelLabel = devPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    devModelLabel:SetPoint("TOPLEFT", devApplyPersonalityButton, "BOTTOMLEFT", 0, -12)
    devModelLabel:SetText("Hatch Model")

    local devModelDropdown
    do
        local function BuildModelEntryLabel(entry)
            local rarityLabel = self.GetMurlocRarityLabel and self:GetMurlocRarityLabel(entry.rarity) or (entry.rarity or "Common")
            local rr, gg, bb = 1, 1, 1
            if self.GetMurlocRarityColor then
                rr, gg, bb = self:GetMurlocRarityColor(entry.rarity)
            end
            local hex = string.format("|cff%02x%02x%02x", math.floor(rr * 255 + 0.5), math.floor(gg * 255 + 0.5), math.floor(bb * 255 + 0.5))
            local name = entry.label or ("Display " .. tostring(entry.displayID))
            return string.format("%s%s|r - %s (%d)", hex, rarityLabel, name, tonumber(entry.displayID) or 0)
        end

        local ok, modernDropdown = pcall(CreateFrame, "DropdownButton", "MurlocMinderDevModelDropdown", devPanel, "WowStyle1DropdownTemplate")
        if ok and modernDropdown and modernDropdown.SetupMenu then
            devModelDropdown = modernDropdown
            devModelDropdown:SetPoint("TOPLEFT", devModelLabel, "BOTTOMLEFT", 0, -2)
            devModelDropdown:SetWidth(DEV_COL2_WIDTH)
            devModelDropdown:SetDefaultText("Weighted Random")
            devModelDropdown:SetupMenu(function(_, rootDescription)
                rootDescription:SetTag("MENU_MURLOC_MINDER_DEV_MODEL")
                local modelPool = self.GetMurlocModelPool and self:GetMurlocModelPool() or {}

                local function IsRandomSelected()
                    self.db.profile.dev = self.db.profile.dev or {}
                    return tonumber(self.db.profile.dev.selectedHatchDisplayID) == nil
                end
                local function SetRandomSelected()
                    self.db.profile.dev = self.db.profile.dev or {}
                    self.db.profile.dev.selectedHatchDisplayID = nil
                    if devModelDropdown.OverrideText then
                        devModelDropdown:OverrideText("Weighted Random")
                    end
                end
                rootDescription:CreateRadio("Weighted Random", IsRandomSelected, SetRandomSelected)

                local grouped = {}
                local rarityOrder = { "common", "uncommon", "rare", "epic", "legendary", "rarest" }
                for _, entry in ipairs(modelPool) do
                    local rarity = tostring(entry.rarity or "common"):lower()
                    grouped[rarity] = grouped[rarity] or {}
                    grouped[rarity][#grouped[rarity] + 1] = entry
                end
                for _, rarity in ipairs(rarityOrder) do
                    local group = grouped[rarity]
                    if group and #group > 0 then
                        table.sort(group, function(a, b)
                            return (tonumber(a.displayID) or 0) < (tonumber(b.displayID) or 0)
                        end)
                        local rarityLabel = self.GetMurlocRarityLabel and self:GetMurlocRarityLabel(rarity) or rarity
                        local submenu = rootDescription:CreateButton(rarityLabel)
                        for _, entry in ipairs(group) do
                            local label = BuildModelEntryLabel(entry)
                            submenu:CreateRadio(label, function()
                                self.db.profile.dev = self.db.profile.dev or {}
                                return tonumber(self.db.profile.dev.selectedHatchDisplayID) == tonumber(entry.displayID)
                            end, function()
                                self.db.profile.dev = self.db.profile.dev or {}
                                self.db.profile.dev.selectedHatchDisplayID = tonumber(entry.displayID)
                                if devModelDropdown.OverrideText then
                                    devModelDropdown:OverrideText(label)
                                end
                            end)
                        end
                    end
                end
            end)
        else
            devModelDropdown = CreateFrame("Frame", "MurlocMinderDevModelDropdown", devPanel, "UIDropDownMenuTemplate")
            devModelDropdown:SetPoint("TOPLEFT", devModelLabel, "BOTTOMLEFT", -14, -2)
            UIDropDownMenu_SetWidth(devModelDropdown, DEV_COL2_WIDTH)
            UIDropDownMenu_JustifyText(devModelDropdown, "LEFT")
            UIDropDownMenu_Initialize(devModelDropdown, function(_, _, _)
                local infoRandom = UIDropDownMenu_CreateInfo()
                infoRandom.text = "Weighted Random"
                infoRandom.value = "__random__"
                infoRandom.func = function()
                    self.db.profile.dev = self.db.profile.dev or {}
                    self.db.profile.dev.selectedHatchDisplayID = nil
                    UIDropDownMenu_SetSelectedValue(devModelDropdown, "__random__")
                    UIDropDownMenu_SetText(devModelDropdown, "Weighted Random")
                end
                UIDropDownMenu_AddButton(infoRandom)

                local pool = self.GetMurlocModelPool and self:GetMurlocModelPool() or {}
                table.sort(pool, function(a, b)
                    return (tonumber(a.displayID) or 0) < (tonumber(b.displayID) or 0)
                end)
                for _, entry in ipairs(pool) do
                    local info = UIDropDownMenu_CreateInfo()
                    local label = string.format("%s (%d)", entry.label or "Display", tonumber(entry.displayID) or 0)
                    info.text = label
                    info.value = tonumber(entry.displayID)
                    info.func = function()
                        self.db.profile.dev = self.db.profile.dev or {}
                        self.db.profile.dev.selectedHatchDisplayID = tonumber(entry.displayID)
                        UIDropDownMenu_SetSelectedValue(devModelDropdown, tonumber(entry.displayID))
                        UIDropDownMenu_SetText(devModelDropdown, label)
                    end
                    UIDropDownMenu_AddButton(info)
                end
            end)
        end
    end

    local devApplyModelButton = CreateFrame("Button", nil, devPanel, "UIPanelButtonTemplate")
    devApplyModelButton:SetSize(DEV_COL2_WIDTH, 22)
    devApplyModelButton:SetPoint("TOPLEFT", devModelDropdown, "BOTTOMLEFT", 0, -2)
    devApplyModelButton:SetText("Apply Model Now")
    SkinButton(devApplyModelButton)
    devApplyModelButton:SetScript("OnClick", function()
        local pet = self:GetPet()
        self.db.profile.dev = self.db.profile.dev or {}
        local selected = tonumber(self.db.profile.dev.selectedHatchDisplayID)
        if selected then
            pet.modelDisplayID = selected
            local entry = self.GetMurlocModelByDisplayID and self:GetMurlocModelByDisplayID(selected) or nil
            if entry then
                pet.modelRarity = entry.rarity or pet.modelRarity or "common"
                pet.modelLabel = entry.label or pet.modelLabel
            end
            self:PushActivity(string.format("Dev: model set to %d.", selected))
        else
            self:PushActivity("Dev: hatch model selection reset to weighted random.")
        end
        if self.frame then
            self.frame._petModelResolved = nil
            self.frame._petModelLoaded = nil
            self.frame._petModelSource = nil
        end
        self:RefreshUI(true)
    end)

    local devAutonomyLabel = devPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    devAutonomyLabel:SetPoint("TOPLEFT", devBody, "TOPLEFT", DEV_COL3_X, -6)
    devAutonomyLabel:SetText("Autonomous Action")

    local devAutonomyDropdown
    do
        local ok, modernDropdown = pcall(CreateFrame, "DropdownButton", "MurlocMinderDevAutonomyDropdown", devPanel, "WowStyle1DropdownTemplate")
        if ok and modernDropdown and modernDropdown.SetupMenu then
            devAutonomyDropdown = modernDropdown
            devAutonomyDropdown:SetPoint("TOPLEFT", devAutonomyLabel, "BOTTOMLEFT", 0, -2)
            devAutonomyDropdown:SetWidth(DEV_COL3_WIDTH)
            devAutonomyDropdown:SetDefaultText("Autonomous Action")
            devAutonomyDropdown:SetupMenu(function(_, rootDescription)
                rootDescription:SetTag("MENU_MURLOC_MINDER_DEV_AUTONOMY")
                local options = self.GetAutonomousBehaviorOptions and self:GetAutonomousBehaviorOptions(true) or {}
                if #options == 0 then
                    rootDescription:CreateTitle("No autonomous actions available")
                    return
                end
                local function IsSelected(option)
                    self.db.profile.dev = self.db.profile.dev or {}
                    return self.db.profile.dev.selectedAutonomyEventKey == option.key
                end
                local function SetSelected(option)
                    self.db.profile.dev = self.db.profile.dev or {}
                    self.db.profile.dev.selectedAutonomyEventKey = option.key
                    if devAutonomyDropdown.OverrideText then
                        devAutonomyDropdown:OverrideText(string.format("%s (%.2f%%)", option.label or option.key or "Unknown", (option.probability or 0) * 100))
                    end
                end
                local function buildLabel(option)
                    return string.format("%s (%.2f%%)", option.label or option.key or "Unknown", (option.probability or 0) * 100)
                end
                local function addOptionList(menu, list)
                    for _, option in ipairs(list) do
                        menu:CreateRadio(buildLabel(option), IsSelected, SetSelected, option)
                    end
                end

                local grouped = {
                    benign = {
                        hunger = {},
                        joy = {},
                        energy = {},
                        cleanliness = {},
                        trait = {},
                        pair = {},
                    },
                    basePositive = {},
                    baseNegative = {},
                    traitPositive = {},
                    traitNegative = {},
                    pairPositiveAll = {},
                    pairNegativeAll = {},
                }

                for _, option in ipairs(options) do
                    local key = option.key or ""
                    if key:find("^benign_hunger_") then
                        grouped.benign.hunger[#grouped.benign.hunger + 1] = option
                    elseif key:find("^benign_joy_") then
                        grouped.benign.joy[#grouped.benign.joy + 1] = option
                    elseif key:find("^benign_energy_") then
                        grouped.benign.energy[#grouped.benign.energy + 1] = option
                    elseif key:find("^benign_cleanliness_") then
                        grouped.benign.cleanliness[#grouped.benign.cleanliness + 1] = option
                    elseif key:find("^benign_trait_") then
                        grouped.benign.trait[#grouped.benign.trait + 1] = option
                    elseif key:find("^benign_pair_") then
                        grouped.benign.pair[#grouped.benign.pair + 1] = option
                    elseif key:find("^trait_pos_") then
                        grouped.traitPositive[#grouped.traitPositive + 1] = option
                    elseif key:find("^trait_neg_") then
                        grouped.traitNegative[#grouped.traitNegative + 1] = option
                    elseif key:find("^pair_pos_") then
                        grouped.pairPositiveAll[#grouped.pairPositiveAll + 1] = option
                    elseif key:find("^pair_neg_") then
                        grouped.pairNegativeAll[#grouped.pairNegativeAll + 1] = option
                    elseif option.polarity == "negative" then
                        grouped.baseNegative[#grouped.baseNegative + 1] = option
                    else
                        grouped.basePositive[#grouped.basePositive + 1] = option
                    end
                end

                addOptionList(rootDescription:CreateButton("All Actions (Active %)"), options)

                local benignMenu = rootDescription:CreateButton("Benign Actions")
                if #grouped.benign.hunger > 0 then
                    addOptionList(benignMenu:CreateButton("Hunger"), grouped.benign.hunger)
                end
                if #grouped.benign.joy > 0 then
                    addOptionList(benignMenu:CreateButton("Joy"), grouped.benign.joy)
                end
                if #grouped.benign.energy > 0 then
                    addOptionList(benignMenu:CreateButton("Energy"), grouped.benign.energy)
                end
                if #grouped.benign.cleanliness > 0 then
                    addOptionList(benignMenu:CreateButton("Cleanliness"), grouped.benign.cleanliness)
                end
                if #grouped.benign.trait > 0 then
                    addOptionList(benignMenu:CreateButton("Trait"), grouped.benign.trait)
                end
                if #grouped.benign.pair > 0 then
                    addOptionList(benignMenu:CreateButton("Trait Pair"), grouped.benign.pair)
                end

                if #grouped.basePositive > 0 then
                    addOptionList(rootDescription:CreateButton("Effect Base Positive"), grouped.basePositive)
                end
                if #grouped.baseNegative > 0 then
                    addOptionList(rootDescription:CreateButton("Effect Base Negative"), grouped.baseNegative)
                end
                if #grouped.traitPositive > 0 then
                    addOptionList(rootDescription:CreateButton("Effect Trait Positive"), grouped.traitPositive)
                end
                if #grouped.traitNegative > 0 then
                    addOptionList(rootDescription:CreateButton("Effect Trait Negative"), grouped.traitNegative)
                end

                local pairPositiveMenu = rootDescription:CreateButton("Effect Pair Positive")
                local pairNegativeMenu = rootDescription:CreateButton("Effect Pair Negative")
                local personalities = self.GetPersonalities and self:GetPersonalities() or {}
                table.sort(personalities, function(a, b)
                    return tostring(a) < tostring(b)
                end)
                local function traitPairListFor(traitName, sourceList)
                    local list = {}
                    local lowerTrait = string.lower(tostring(traitName or ""))
                    for _, option in ipairs(sourceList or {}) do
                        local key = tostring(option.key or "")
                        local a, b = key:match("^pair_%a+_([^_]+)_([^_]+)$")
                        if a and b and (a == lowerTrait or b == lowerTrait) then
                            list[#list + 1] = option
                        end
                    end
                    return list
                end
                for _, traitName in ipairs(personalities) do
                    local list = traitPairListFor(traitName, grouped.pairPositiveAll)
                    local submenu = pairPositiveMenu:CreateButton(traitName)
                    if #list > 0 then
                        addOptionList(submenu, list)
                    else
                        submenu:CreateButton("No pair actions")
                    end
                end
                for _, traitName in ipairs(personalities) do
                    local list = traitPairListFor(traitName, grouped.pairNegativeAll)
                    local submenu = pairNegativeMenu:CreateButton(traitName)
                    if #list > 0 then
                        addOptionList(submenu, list)
                    else
                        submenu:CreateButton("No pair actions")
                    end
                end
            end)
        else
            devAutonomyDropdown = CreateFrame("Frame", "MurlocMinderDevAutonomyDropdown", devPanel, "UIDropDownMenuTemplate")
            devAutonomyDropdown:SetPoint("TOPLEFT", devAutonomyLabel, "BOTTOMLEFT", -14, -2)
            UIDropDownMenu_SetWidth(devAutonomyDropdown, DEV_COL3_WIDTH)
            UIDropDownMenu_JustifyText(devAutonomyDropdown, "LEFT")
            UIDropDownMenu_Initialize(devAutonomyDropdown, function(_, _, _)
                local options = self.GetAutonomousBehaviorOptions and self:GetAutonomousBehaviorOptions(true) or {}
                if #options == 0 then
                    local info = UIDropDownMenu_CreateInfo()
                    info.text = "No autonomous actions available"
                    info.notCheckable = true
                    info.disabled = true
                    UIDropDownMenu_AddButton(info)
                    return
                end
                for _, option in ipairs(options) do
                    local info = UIDropDownMenu_CreateInfo()
                    local displayText = string.format("%s (%.2f%%)", option.label or option.key or "Unknown", (option.probability or 0) * 100)
                    info.text = displayText
                    info.value = option.key
                    info.func = function()
                        self.db.profile.dev = self.db.profile.dev or {}
                        self.db.profile.dev.selectedAutonomyEventKey = option.key
                        UIDropDownMenu_SetSelectedValue(devAutonomyDropdown, option.key)
                        UIDropDownMenu_SetText(devAutonomyDropdown, displayText)
                    end
                    UIDropDownMenu_AddButton(info)
                end
            end)
        end
    end

    local devTriggerAutonomyButton = CreateFrame("Button", nil, devPanel, "UIPanelButtonTemplate")
    devTriggerAutonomyButton:SetSize(170, 22)
    devTriggerAutonomyButton:SetPoint("TOPLEFT", devAutonomyDropdown, "BOTTOMLEFT", 0, -2)
    devTriggerAutonomyButton:SetText("Trigger Selected Auto")
    SkinButton(devTriggerAutonomyButton)
    devTriggerAutonomyButton:SetScript("OnClick", function()
        local options = self.GetAutonomousBehaviorOptions and self:GetAutonomousBehaviorOptions(true) or {}
        if #options == 0 then
            self:PushActivity("Dev: no autonomous actions available.")
            return
        end

        self.db.profile.dev = self.db.profile.dev or {}
        local chosenKey = self.db.profile.dev.selectedAutonomyEventKey
        if not chosenKey or chosenKey == "" then
            chosenKey = options[1].key
            self.db.profile.dev.selectedAutonomyEventKey = chosenKey
        end

        if self.TriggerAutonomousBehaviorByKey then
            local ok, msg = self:TriggerAutonomousBehaviorByKey(chosenKey)
            if not ok and msg and msg ~= "" then
                self:PushActivity("Dev: " .. msg)
            end
            self:RefreshUI(true)
        end
    end)

    local devBgStageLabel = devPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    devBgStageLabel:SetPoint("TOPLEFT", devTriggerAutonomyButton, "BOTTOMLEFT", 0, -14)
    devBgStageLabel:SetText("Background Stage")

    local devBgChoiceDropdown
    local devBgStageDropdown
    do
        local ok, modernDropdown = pcall(CreateFrame, "DropdownButton", "MurlocMinderDevBgStageDropdown", devPanel, "WowStyle1DropdownTemplate")
        if ok and modernDropdown and modernDropdown.SetupMenu then
            devBgStageDropdown = modernDropdown
            devBgStageDropdown:SetPoint("TOPLEFT", devBgStageLabel, "BOTTOMLEFT", 0, -2)
            devBgStageDropdown:SetWidth(170)
            devBgStageDropdown:SetDefaultText("Stage")
            devBgStageDropdown:SetupMenu(function(_, rootDescription)
                rootDescription:SetTag("MENU_MURLOC_MINDER_DEV_BG_STAGE")
                local stageOptions = {
                    { key = "egg", label = "Egg" },
                    { key = "adolescent", label = "Adolescent+" },
                }
                local function IsSelected(option)
                    self.db.profile.dev = self.db.profile.dev or {}
                    return (self.db.profile.dev.selectedBackgroundStage or "egg") == option.key
                end
                local function SetSelected(option)
                    self.db.profile.dev = self.db.profile.dev or {}
                    self.db.profile.dev.selectedBackgroundStage = option.key
                    if devBgStageDropdown.OverrideText then
                        devBgStageDropdown:OverrideText(option.label)
                    end
                    self.db.profile.dev.stageBackgroundOverrides = self.db.profile.dev.stageBackgroundOverrides or {}
                    local stageChoice = self.db.profile.dev.stageBackgroundOverrides[option.key] or "auto"
                    self.db.profile.dev.selectedBackgroundChoice = stageChoice
                    if devBgChoiceDropdown and devBgChoiceDropdown.OverrideText then
                        devBgChoiceDropdown:OverrideText(GetDevBackgroundLabel(stageChoice))
                    elseif devBgChoiceDropdown then
                        UIDropDownMenu_SetSelectedValue(devBgChoiceDropdown, stageChoice)
                        UIDropDownMenu_SetText(devBgChoiceDropdown, GetDevBackgroundLabel(stageChoice))
                    end
                end
                for _, option in ipairs(stageOptions) do
                    rootDescription:CreateRadio(option.label, IsSelected, SetSelected, option)
                end
            end)
        else
            devBgStageDropdown = CreateFrame("Frame", "MurlocMinderDevBgStageDropdown", devPanel, "UIDropDownMenuTemplate")
            devBgStageDropdown:SetPoint("TOPLEFT", devBgStageLabel, "BOTTOMLEFT", -14, -2)
            UIDropDownMenu_SetWidth(devBgStageDropdown, 170)
            UIDropDownMenu_JustifyText(devBgStageDropdown, "LEFT")
            UIDropDownMenu_Initialize(devBgStageDropdown, function(_, _, _)
                local stageOptions = {
                    { key = "egg", label = "Egg" },
                    { key = "adolescent", label = "Adolescent+" },
                }
                for _, option in ipairs(stageOptions) do
                    local info = UIDropDownMenu_CreateInfo()
                    info.text = option.label
                    info.value = option.key
                    info.func = function()
                        self.db.profile.dev = self.db.profile.dev or {}
                        self.db.profile.dev.selectedBackgroundStage = option.key
                        UIDropDownMenu_SetSelectedValue(devBgStageDropdown, option.key)
                        UIDropDownMenu_SetText(devBgStageDropdown, option.label)
                        self.db.profile.dev.stageBackgroundOverrides = self.db.profile.dev.stageBackgroundOverrides or {}
                        local stageChoice = self.db.profile.dev.stageBackgroundOverrides[option.key] or "auto"
                        self.db.profile.dev.selectedBackgroundChoice = stageChoice
                        UIDropDownMenu_SetSelectedValue(devBgChoiceDropdown, stageChoice)
                        UIDropDownMenu_SetText(devBgChoiceDropdown, GetDevBackgroundLabel(stageChoice))
                    end
                    UIDropDownMenu_AddButton(info)
                end
            end)
        end
    end

    local devBgChoiceLabel = devPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    devBgChoiceLabel:SetPoint("TOPLEFT", devBgStageDropdown, "BOTTOMLEFT", 0, -2)
    devBgChoiceLabel:SetText("Background")

    if not devBgChoiceDropdown then
        local ok, modernDropdown = pcall(CreateFrame, "DropdownButton", "MurlocMinderDevBgChoiceDropdown", devPanel, "WowStyle1DropdownTemplate")
        if ok and modernDropdown and modernDropdown.SetupMenu then
            devBgChoiceDropdown = modernDropdown
            devBgChoiceDropdown:SetPoint("TOPLEFT", devBgChoiceLabel, "BOTTOMLEFT", 0, -2)
            devBgChoiceDropdown:SetWidth(170)
            devBgChoiceDropdown:SetDefaultText("Background")
            devBgChoiceDropdown:SetupMenu(function(_, rootDescription)
                rootDescription:SetTag("MENU_MURLOC_MINDER_DEV_BG_CHOICE")
                local function IsSelected(entry)
                    self.db.profile.dev = self.db.profile.dev or {}
                    return (self.db.profile.dev.selectedBackgroundChoice or "auto") == entry.key
                end
                local function SetSelected(entry)
                    self.db.profile.dev = self.db.profile.dev or {}
                    self.db.profile.dev.selectedBackgroundChoice = entry.key
                    if devBgChoiceDropdown.OverrideText then
                        devBgChoiceDropdown:OverrideText(entry.label)
                    end
                end
                for _, entry in ipairs(DEV_BACKGROUND_CHOICES) do
                    rootDescription:CreateRadio(entry.label, IsSelected, SetSelected, entry)
                end
            end)
        else
            devBgChoiceDropdown = CreateFrame("Frame", "MurlocMinderDevBgChoiceDropdown", devPanel, "UIDropDownMenuTemplate")
            devBgChoiceDropdown:SetPoint("TOPLEFT", devBgChoiceLabel, "BOTTOMLEFT", -14, -2)
            UIDropDownMenu_SetWidth(devBgChoiceDropdown, 170)
            UIDropDownMenu_JustifyText(devBgChoiceDropdown, "LEFT")
            UIDropDownMenu_Initialize(devBgChoiceDropdown, function(_, _, _)
                for _, entry in ipairs(DEV_BACKGROUND_CHOICES) do
                    local info = UIDropDownMenu_CreateInfo()
                    info.text = entry.label
                    info.value = entry.key
                    info.func = function()
                        self.db.profile.dev = self.db.profile.dev or {}
                        self.db.profile.dev.selectedBackgroundChoice = entry.key
                        UIDropDownMenu_SetSelectedValue(devBgChoiceDropdown, entry.key)
                        UIDropDownMenu_SetText(devBgChoiceDropdown, entry.label)
                    end
                    UIDropDownMenu_AddButton(info)
                end
            end)
        end
    end

    local devApplyBgButton = CreateFrame("Button", nil, devPanel, "UIPanelButtonTemplate")
    devApplyBgButton:SetSize(170, 22)
    devApplyBgButton:SetPoint("TOPLEFT", devBgChoiceDropdown, "BOTTOMLEFT", 0, -2)
    devApplyBgButton:SetText("Apply Background")
    SkinButton(devApplyBgButton)
    devApplyBgButton:SetScript("OnClick", function()
        self.db.profile.dev = self.db.profile.dev or {}
        self.db.profile.dev.stageBackgroundOverrides = self.db.profile.dev.stageBackgroundOverrides or {}
        local stageKey = self.db.profile.dev.selectedBackgroundStage or ((self:GetPet().stage == "egg") and "egg" or "adolescent")
        if stageKey ~= "egg" and stageKey ~= "adolescent" then
            stageKey = (self:GetPet().stage == "egg") and "egg" or "adolescent"
        end
        local choiceKey = self.db.profile.dev.selectedBackgroundChoice or "auto"
        if not DEV_BACKGROUND_CHOICES_BY_KEY[choiceKey] then
            choiceKey = "auto"
        end
        self.db.profile.dev.stageBackgroundOverrides[stageKey] = choiceKey
        self:PushActivity(string.format("Dev: %s background set to %s.", stageKey, GetDevBackgroundLabel(choiceKey)))
        self:RefreshUI(true)
    end)

    local devChallengeCompleteLabel = devPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    devChallengeCompleteLabel:SetPoint("TOPLEFT", devApplyBgButton, "BOTTOMLEFT", 0, -14)
    devChallengeCompleteLabel:SetText("Complete Challenge Slot")

    local devChallengeCompleteChecks = {}
    for i = 1, 4 do
        local check = CreateFrame("CheckButton", nil, devPanel, "UICheckButtonTemplate")
        local col = (i - 1) % 2
        local row = math.floor((i - 1) / 2)
        check:SetPoint("TOPLEFT", devChallengeCompleteLabel, "BOTTOMLEFT", col * 88, -(row * 22))
        local checkText = check.Text
        if not checkText and check.GetName and check:GetName() then
            checkText = _G[check:GetName() .. "Text"]
        end
        if checkText then
            checkText:SetText(string.format("Slot %d", i))
        end
        check:SetScript("OnClick", function(btn)
            if not btn:GetChecked() then
                return
            end
            if self.DevCompleteChallengeSlot then
                local ok, msg = self:DevCompleteChallengeSlot(i)
                if msg and msg ~= "" then
                    self:Print(msg)
                end
                if ok then
                    self:RefreshUI(true)
                end
            end
            btn:SetChecked(false)
        end)
        devChallengeCompleteChecks[i] = check
    end

    local activityBox = CreateCard(habitat, activityPaneWidth, 112, "BOTTOMLEFT", habitat, "BOTTOMLEFT", 10, 10)
    activityBox:SetFrameStrata(frame:GetFrameStrata())
    activityBox:SetFrameLevel(habitat:GetFrameLevel() + 80)
    activityBox:SetToplevel(true)
    activityBox:SetClipsChildren(true)

    local activityText = CreateFrame("ScrollingMessageFrame", nil, activityBox)
    activityText:SetPoint("TOPLEFT", activityBox, "TOPLEFT", 10, -10)
    activityText:SetPoint("BOTTOMRIGHT", activityBox, "BOTTOMRIGHT", -10, 10)
    activityText:SetFrameStrata(activityBox:GetFrameStrata())
    activityText:SetFrameLevel(activityBox:GetFrameLevel() + 1)
    activityText:SetFontObject(GameFontNormal)
    activityText:SetJustifyH("LEFT")
    activityText:SetFading(false)
    activityText:SetMaxLines(200)
    activityText:SetInsertMode("BOTTOM")
    activityText:EnableMouseWheel(true)
    activityText:SetScript("OnMouseWheel", function(self, delta)
        if delta > 0 then
            self:ScrollUp()
        else
            self:ScrollDown()
        end
    end)

    local welcome = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    welcome:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -28)
    welcome:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -8, 8)
    welcome:SetFrameStrata("HIGH")
    welcome:SetFrameLevel(math.max((frame:GetFrameLevel() or 1) + 120, 120))
    welcome:SetToplevel(true)
    welcome:EnableMouse(true)
    if welcome.SetPropagateMouseClicks then
        welcome:SetPropagateMouseClicks(false)
    end
    welcome:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 14,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    welcome:SetBackdropColor(0.01, 0.03, 0.05, 0.18)

    local tutorialMaskFull = welcome:CreateTexture(nil, "ARTWORK", nil, 1)
    tutorialMaskFull:SetAllPoints(true)
    tutorialMaskFull:SetColorTexture(0.0, 0.0, 0.0, TUTORIAL_MASK_ALPHA)

    local function CreateTutorialMask()
        local mask = welcome:CreateTexture(nil, "ARTWORK", nil, 1)
        mask:SetColorTexture(0.0, 0.0, 0.0, TUTORIAL_MASK_ALPHA)
        mask:Hide()
        return mask
    end

    local tutorialMaskTop = CreateTutorialMask()
    local tutorialMaskBottom = CreateTutorialMask()
    local tutorialMaskLeft = CreateTutorialMask()
    local tutorialMaskRight = CreateTutorialMask()

    local tutorialHighlight = CreateFrame("Frame", nil, welcome, "BackdropTemplate")
    tutorialHighlight:SetFrameStrata(welcome:GetFrameStrata())
    tutorialHighlight:SetFrameLevel(welcome:GetFrameLevel() + 14)
    tutorialHighlight:SetBackdrop({
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 14,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    tutorialHighlight:SetBackdropColor(0, 0, 0, 0)
    tutorialHighlight:SetBackdropBorderColor(1.0, 0.84, 0.10, 1.0)
    local tutorialHighlightFill = tutorialHighlight:CreateTexture(nil, "BACKGROUND")
    tutorialHighlightFill:SetAllPoints(true)
    tutorialHighlightFill:SetColorTexture(1.0, 0.82, 0.12, 0.08)
    tutorialHighlight:Hide()
    do
        local pulse = tutorialHighlight:CreateAnimationGroup()
        pulse:SetLooping("REPEAT")
        local fadeOut = pulse:CreateAnimation("Alpha")
        fadeOut:SetOrder(1)
        fadeOut:SetDuration(0.46)
        fadeOut:SetFromAlpha(1.0)
        fadeOut:SetToAlpha(0.48)
        local fadeIn = pulse:CreateAnimation("Alpha")
        fadeIn:SetOrder(2)
        fadeIn:SetDuration(0.46)
        fadeIn:SetFromAlpha(0.48)
        fadeIn:SetToAlpha(1.0)
        tutorialHighlight.Pulse = pulse
    end

    local tutorialCard = CreateFrame("Frame", nil, welcome, "BackdropTemplate")
    tutorialCard:SetPoint("TOPLEFT", habitat, "TOPLEFT", 12, -12)
    tutorialCard:SetPoint("TOPRIGHT", habitat, "TOPRIGHT", -12, -12)
    tutorialCard:SetHeight(178)
    tutorialCard:SetFrameStrata(welcome:GetFrameStrata())
    tutorialCard:SetFrameLevel(welcome:GetFrameLevel() + 24)
    tutorialCard:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 12,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    tutorialCard:SetBackdropColor(0.02, 0.06, 0.10, 0.96)
    tutorialCard:SetBackdropBorderColor(0.22, 0.52, 0.62, 0.9)

    local tutorialTitle = tutorialCard:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    tutorialTitle:SetPoint("TOPLEFT", tutorialCard, "TOPLEFT", 18, -16)
    tutorialTitle:SetPoint("TOPRIGHT", tutorialCard, "TOPRIGHT", -18, -16)
    tutorialTitle:SetJustifyH("LEFT")
    tutorialTitle:SetText("Welcome to Murloc Minder")

    local tutorialBody = tutorialCard:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    tutorialBody:SetPoint("TOPLEFT", tutorialTitle, "BOTTOMLEFT", 0, -12)
    tutorialBody:SetPoint("TOPRIGHT", tutorialTitle, "BOTTOMRIGHT", 0, -12)
    tutorialBody:SetPoint("BOTTOM", tutorialCard, "BOTTOM", 0, 52)
    tutorialBody:SetJustifyH("LEFT")
    tutorialBody:SetJustifyV("TOP")
    tutorialBody:SetText("")

    local tutorialStepText = tutorialCard:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    tutorialStepText:SetPoint("BOTTOMLEFT", tutorialCard, "BOTTOMLEFT", 18, 18)
    tutorialStepText:SetText("Step 1 of 5")

    local tutorialNextButton = CreateFrame("Button", nil, tutorialCard, "UIPanelButtonTemplate")
    tutorialNextButton:SetSize(160, 30)
    tutorialNextButton:SetPoint("BOTTOMRIGHT", tutorialCard, "BOTTOMRIGHT", -18, 12)
    tutorialNextButton:SetText("Next")
    SkinButton(tutorialNextButton)
    tutorialNextButton:SetScript("OnClick", function()
        if self.PlayUISound then
            self:PlayUISound("button")
        end
        self:AdvanceWelcomeTutorial()
    end)

    welcome._mmStepIndex = 1
    welcome.TutorialMaskFull = tutorialMaskFull
    welcome.TutorialMaskTop = tutorialMaskTop
    welcome.TutorialMaskBottom = tutorialMaskBottom
    welcome.TutorialMaskLeft = tutorialMaskLeft
    welcome.TutorialMaskRight = tutorialMaskRight
    welcome.TutorialHighlightBox = tutorialHighlight
    welcome.TutorialCard = tutorialCard
    welcome.TutorialTitle = tutorialTitle
    welcome.TutorialBody = tutorialBody
    welcome.TutorialStepText = tutorialStepText
    welcome.TutorialNextButton = tutorialNextButton
    welcome:HookScript("OnHide", function(panel)
        if panel.TutorialHighlightBox and panel.TutorialHighlightBox.Pulse and panel.TutorialHighlightBox.Pulse:IsPlaying() then
            panel.TutorialHighlightBox.Pulse:Stop()
        end
        if self.PlayUISound then
            self:PlayUISound("popup_close")
        end
    end)
    welcome:SetScript("OnShow", function(panel)
        panel:Raise()
        if self.PlayUISound then
            self:PlayUISound("popup_open")
        end
        panel._mmStepIndex = ClampNumber(panel._mmStepIndex or 1, 1, #self:GetWelcomeTutorialSteps())
        self:RefreshWelcomeTutorial()
    end)

    self.db.profile.ui = self.db.profile.ui or {}
    if self.db.profile.ui.trackerMode == nil then
        if self.db.profile.ui.trackerChallengesCollapsed == true then
            self.db.profile.ui.trackerMode = "half"
        else
            self.db.profile.ui.trackerMode = "full"
        end
    end
    local tracker = CreateCard(UIParent, 370, 270, "TOPRIGHT", UIParent, "TOPRIGHT", -120, -220)
    tracker:SetSize(370, 270)
    tracker:SetFrameStrata("FULLSCREEN_DIALOG")
    tracker:SetFrameLevel(200)
    tracker:SetToplevel(true)
    tracker:EnableMouse(true)
    tracker:SetMovable(true)
    tracker:SetClampedToScreen(true)
    tracker:RegisterForDrag("LeftButton")
    tracker:SetScript("OnDragStart", function(f)
        f._mmDragging = true
        f:StartMoving()
    end)
    tracker:SetScript("OnDragStop", function(f)
        f:StopMovingOrSizing()
        f._mmDragging = false
        f._mmDragged = true
        self.db.profile.ui = self.db.profile.ui or {}
        local p, _, _, x, y = f:GetPoint(1)
        self.db.profile.ui.trackerPoint = p or "TOPRIGHT"
        self.db.profile.ui.trackerX = math.floor(x or 0)
        self.db.profile.ui.trackerY = math.floor(y or 0)
    end)
    do
        local p = self.db.profile.ui.trackerPoint
        local x = self.db.profile.ui.trackerX
        local y = self.db.profile.ui.trackerY
        if p and x and y then
            tracker:ClearAllPoints()
            tracker:SetPoint(p, UIParent, p, x, y)
        end
    end
    tracker:SetScript("OnMouseUp", function(_, button)
        if button ~= "LeftButton" then
            return
        end
        if tracker._mmSuppressOpen then
            tracker._mmSuppressOpen = false
            return
        end
        if tracker._mmDragged then
            tracker._mmDragged = false
            return
        end
        self:ShowMainFrame()
    end)
    tracker:SetScript("OnEnter", function(owner)
        GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
        GameTooltip:SetText("Murloc Minder Tracker")
        GameTooltip:AddLine("Left Click: Open full panel", 1, 1, 1)
        GameTooltip:Show()
    end)
    tracker:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    local trackerTitle = tracker:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    trackerTitle:SetPoint("TOPLEFT", tracker, "TOPLEFT", 10, -8)
    trackerTitle:SetText("Murloc Tracker")

    local trackerMiniModeButton = CreateFrame("Button", nil, tracker, "UIPanelHideButtonNoScripts")
    trackerMiniModeButton:SetSize(17, 17)
    trackerMiniModeButton:SetPoint("TOPRIGHT", tracker, "TOPRIGHT", -10, -6)
    trackerMiniModeButton:SetScript("OnMouseDown", function()
        tracker._mmSuppressOpen = true
    end)
    trackerMiniModeButton:SetScript("OnClick", function()
        self.db.profile.ui = self.db.profile.ui or {}
        self.db.profile.ui.trackerMode = "mini"
        self:RefreshUI(true)
    end)

    local trackerName = tracker:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    trackerName:SetPoint("TOPLEFT", tracker, "TOPLEFT", 10, -9)
    trackerName:SetWidth(170)
    trackerName:SetJustifyH("LEFT")
    trackerName:SetTextColor(1.0, 0.82, 0.1)
    trackerName:SetText("")
    self:BindPetIdentityTooltip(trackerName)

    local trackerShellsText = tracker:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    trackerShellsText:SetPoint("TOP", tracker, "TOP", 0, -9)
    trackerShellsText:SetJustifyH("CENTER")
    trackerShellsText:SetText(ColorizeShellText("Shells: 0"))

    local trackerNeedDivider = tracker:CreateTexture(nil, "BORDER")
    trackerNeedDivider:SetPoint("TOPLEFT", tracker, "TOPLEFT", 10, -28)
    trackerNeedDivider:SetPoint("TOPRIGHT", tracker, "TOPRIGHT", -10, -28)
    trackerNeedDivider:SetHeight(1)
    trackerNeedDivider:SetColorTexture(0.22, 0.52, 0.62, 0.65)

    local function CreateTrackerNeedCell(parent, label, color, col, row)
        local cell = CreateFrame("Button", nil, parent)
        local cellWidth = 170
        local cellHeight = 44
        local gapX = 10
        local gapY = 8
        cell:SetSize(cellWidth, cellHeight)
        cell:SetPoint("TOPLEFT", parent, "TOPLEFT", 10 + (col * (cellWidth + gapX)), -34 - (row * (cellHeight + gapY)))
        cell:RegisterForClicks("LeftButtonUp")
        cell:SetScript("OnClick", function()
            self:ShowMainFrame()
        end)

        local bg = cell:CreateTexture(nil, "BACKGROUND")
        bg:SetAllPoints(true)
        bg:SetColorTexture(0.08, 0.13, 0.18, 0.42)

        local title = cell:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        title:SetPoint("TOPLEFT", cell, "TOPLEFT", 4, -2)
        title:SetText(label)

        local value = cell:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        value:SetPoint("TOPRIGHT", cell, "TOPRIGHT", -4, -2)
        value:SetText("0%")

        local bar = CreateFrame("StatusBar", nil, cell)
        bar:SetPoint("BOTTOMLEFT", cell, "BOTTOMLEFT", 4, 4)
        bar:SetPoint("BOTTOMRIGHT", cell, "BOTTOMRIGHT", -4, 4)
        bar:SetHeight(14)
        bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
        bar:SetMinMaxValues(0, 100)
        bar:SetStatusBarColor(color[1], color[2], color[3])

        local barBG = bar:CreateTexture(nil, "BACKGROUND")
        barBG:SetAllPoints(true)
        barBG:SetColorTexture(0.07, 0.09, 0.12, 0.95)

        cell.title = title
        cell.value = value
        cell.bar = bar
        return cell
    end

    local trackerNeedCells = {
        hunger = CreateTrackerNeedCell(tracker, "Hunger", COLOR_HUNGER, 0, 0),
        joy = CreateTrackerNeedCell(tracker, "Joy", COLOR_JOY, 1, 0),
        energy = CreateTrackerNeedCell(tracker, "Energy", COLOR_ENERGY, 0, 1),
        cleanliness = CreateTrackerNeedCell(tracker, "Cleanliness", COLOR_CLEAN, 1, 1),
    }

    local function SetNeedTooltip(cell, needKey)
        cell:SetScript("OnEnter", function(owner)
            local petData = self:GetPet()
            local value = math.floor(((petData and petData[needKey]) or 0) + 0.5)
            GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
            GameTooltip:AddLine(owner.title and owner.title:GetText() or "Need", 1, 1, 1)
            GameTooltip:AddLine(string.format("Current: %d%%", value), 0.92, 0.92, 0.92)
            GameTooltip:AddLine("Left Click: Open full panel", 0.75, 0.85, 1)
            GameTooltip:Show()
        end)
        cell:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)
    end
    SetNeedTooltip(trackerNeedCells.hunger, "hunger")
    SetNeedTooltip(trackerNeedCells.joy, "joy")
    SetNeedTooltip(trackerNeedCells.energy, "energy")
    SetNeedTooltip(trackerNeedCells.cleanliness, "cleanliness")

    local trackerEggCell = CreateFrame("Button", nil, tracker)
    trackerEggCell:SetSize(350, 100)
    trackerEggCell:SetPoint("TOPLEFT", tracker, "TOPLEFT", 10, -34)
    trackerEggCell:RegisterForClicks("LeftButtonUp")
    trackerEggCell:SetScript("OnClick", function()
        self:ShowMainFrame()
    end)
    local trackerEggBG = trackerEggCell:CreateTexture(nil, "BACKGROUND")
    trackerEggBG:SetAllPoints(true)
    trackerEggBG:SetColorTexture(0.08, 0.13, 0.18, 0.42)
    local trackerEggTitle = trackerEggCell:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    trackerEggTitle:SetPoint("TOPLEFT", trackerEggCell, "TOPLEFT", 4, -2)
    trackerEggTitle:SetText("Hatch Progress")
    local trackerEggValue = trackerEggCell:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    trackerEggValue:SetPoint("TOPRIGHT", trackerEggCell, "TOPRIGHT", -4, -2)
    trackerEggValue:SetText("0%")
    local trackerEggBar = CreateFrame("StatusBar", nil, trackerEggCell)
    trackerEggBar:SetPoint("LEFT", trackerEggCell, "LEFT", 4, 0)
    trackerEggBar:SetPoint("RIGHT", trackerEggCell, "RIGHT", -4, 0)
    trackerEggBar:SetPoint("CENTER", trackerEggCell, "CENTER", 0, 0)
    trackerEggBar:SetHeight(math.floor((trackerEggCell:GetHeight() * 0.25) + 0.5))
    trackerEggBar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
    trackerEggBar:SetMinMaxValues(0, 100)
    trackerEggBar:SetStatusBarColor(COLOR_HATCH[1], COLOR_HATCH[2], COLOR_HATCH[3])
    local trackerEggBarBG = trackerEggBar:CreateTexture(nil, "BACKGROUND")
    trackerEggBarBG:SetAllPoints(true)
    trackerEggBarBG:SetColorTexture(0.07, 0.09, 0.12, 0.95)
    trackerEggCell:SetScript("OnEnter", function(owner)
        local petData = self:GetPet()
        local value = math.floor(((petData and petData.hatchProgress) or 0) + 0.5)
        GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
        GameTooltip:AddLine("Hatch Progress", 1, 1, 1)
        GameTooltip:AddLine(string.format("Current: %d%%", value), 0.92, 0.92, 0.92)
        GameTooltip:AddLine("Complete care actions and challenges to hatch.", COLOR_HATCH[1], COLOR_HATCH[2], COLOR_HATCH[3], true)
        GameTooltip:AddLine("Left Click: Open full panel", 0.75, 0.85, 1)
        GameTooltip:Show()
    end)
    trackerEggCell:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    trackerEggCell:Hide()

    local trackerChallengeDivider = tracker:CreateTexture(nil, "BORDER")
    trackerChallengeDivider:SetPoint("TOPLEFT", tracker, "TOPLEFT", 10, -138)
    trackerChallengeDivider:SetPoint("TOPRIGHT", tracker, "TOPRIGHT", -10, -138)
    trackerChallengeDivider:SetHeight(1)
    trackerChallengeDivider:SetColorTexture(0.22, 0.52, 0.62, 0.65)

    local trackerChallengeTitle = tracker:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    trackerChallengeTitle:SetPoint("TOPLEFT", tracker, "TOPLEFT", 10, -151)
    trackerChallengeTitle:SetText("Active Challenges")
    local trackerScavengeStatus = tracker:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    trackerScavengeStatus:SetPoint("TOPRIGHT", tracker, "TOPRIGHT", -10, -151)
    trackerScavengeStatus:SetJustifyH("RIGHT")
    trackerScavengeStatus:SetText("Scavenging: Inactive")
    local trackerChallengeToggle = CreateFrame("Button", nil, tracker, "UIPanelHideButtonNoScripts")
    trackerChallengeToggle:SetSize(15, 15)
    trackerChallengeToggle:SetPoint("LEFT", trackerChallengeTitle, "RIGHT", 6, 0)
    trackerChallengeToggle:SetScript("OnClick", function()
        self.db.profile.ui = self.db.profile.ui or {}
        self.db.profile.ui.trackerChallengesCollapsed = not (self.db.profile.ui.trackerChallengesCollapsed == true)
        self.db.profile.ui.trackerMode = (self.db.profile.ui.trackerChallengesCollapsed == true) and "half" or "full"
        self:RefreshUI(true)
    end)

    local trackerBasicButtons = {}
    do
        local defs = {
            { key = "feed", label = "Basic Feed", color = COLOR_HUNGER, row = 1, section = "feed" },
            { key = "play", label = "Basic Play", color = COLOR_JOY, row = 2, section = "play" },
            { key = "rest", label = "Basic Rest", color = COLOR_ENERGY, row = 3, section = "rest" },
            { key = "clean", label = "Basic Clean", color = COLOR_CLEAN, row = 4, section = "clean" },
            { key = "egg_soothe", label = "Soothe Egg", row = 1, eggAction = "soothe" },
            { key = "egg_warm", label = "Warm Egg", row = 2, eggAction = "warm" },
            { key = "egg_clean", label = "Clean Egg", row = 3, eggAction = "clean" },
        }
        for _, def in ipairs(defs) do
            local btn = CreateFrame("Button", nil, tracker, "UIPanelButtonTemplate")
            btn:SetSize(104, 18)
            btn:SetPoint("TOPRIGHT", tracker, "TOPRIGHT", -10, -168 - ((def.row - 1) * 19))
            btn._mmSection = def.section
            btn._mmEggAction = def.eggAction
            btn._mmLabel = def.label
            btn._mmColor = def.color
            SkinButton(btn)
            btn:SetScript("OnClick", function()
                local soundKey = def.section
                if def.eggAction then
                    soundKey = "egg_" .. tostring(def.eggAction)
                end
                if self.PlayCareActionButtonSound then
                    self:PlayCareActionButtonSound(soundKey)
                end
                local ok, msg
                if def.eggAction and self.CareForEgg then
                    ok, msg = self:CareForEgg(def.eggAction)
                elseif def.section == "feed" then
                    ok, msg = self:FeedFood("__free__")
                elseif def.section == "play" then
                    ok, msg = self:PlayWithMurloc("__free__")
                elseif def.section == "rest" then
                    ok, msg = self:RestMurloc("__free__")
                elseif def.section == "clean" then
                    ok, msg = self:CleanHabitat("__free__")
                end
                if ok == false and msg then
                    self:PushActivity(msg)
                    self:RefreshUI()
                end
            end)
            trackerBasicButtons[def.key] = btn
        end
    end

    local trackerHalfModeButton = CreateFrame("Button", nil, tracker)
    trackerHalfModeButton:SetSize(18, 19)
    trackerHalfModeButton:SetPoint("TOPRIGHT", tracker, "TOPRIGHT", -30, -6)
    trackerHalfModeButton:SetNormalAtlas("ui-questtrackerbutton-expand-all", true)
    trackerHalfModeButton:SetPushedAtlas("ui-questtrackerbutton-expand-all-pressed", true)
    trackerHalfModeButton:SetHighlightAtlas("ui-questtrackerbutton-red-highlight", "ADD")
    trackerHalfModeButton:SetScript("OnMouseDown", function()
        tracker._mmSuppressOpen = true
    end)
    trackerHalfModeButton:SetScript("OnClick", function()
        self.db.profile.ui = self.db.profile.ui or {}
        self.db.profile.ui.trackerMode = "half"
        self.db.profile.ui.trackerChallengesCollapsed = true
        self:RefreshUI(true)
    end)

    local trackerFullModeButton = CreateFrame("Button", nil, tracker)
    trackerFullModeButton:SetSize(17, 17)
    trackerFullModeButton:SetPoint("TOPRIGHT", tracker, "TOPRIGHT", -10, -6)
    trackerFullModeButton:SetNormalAtlas("RedButton-Expand")
    trackerFullModeButton:SetPushedAtlas("RedButton-Expand-Pressed")
    trackerFullModeButton:SetDisabledAtlas("RedButton-Expand-Disabled")
    trackerFullModeButton:SetHighlightAtlas("RedButton-Highlight", "ADD")
    trackerFullModeButton:SetScript("OnMouseDown", function()
        tracker._mmSuppressOpen = true
    end)
    trackerFullModeButton:SetScript("OnClick", function()
        self.db.profile.ui = self.db.profile.ui or {}
        self.db.profile.ui.trackerMode = "full"
        self.db.profile.ui.trackerChallengesCollapsed = false
        self:RefreshUI(true)
    end)

    local trackerChallengeRows = {}
    for i = 1, 4 do
        local row = CreateFrame("Button", nil, tracker)
        row:SetSize(236, 18)
        row:SetPoint("TOPLEFT", tracker, "TOPLEFT", 10, -168 - ((i - 1) * 19))
        row:RegisterForClicks("LeftButtonUp")
        row:SetScript("OnClick", function()
            self:ShowMainFrame()
        end)
        row:SetScript("OnEnter", function(owner)
            self:ShowChallengeTooltip(i, owner)
        end)
        row:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)

        local text = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        text:SetPoint("LEFT", row, "LEFT", 4, 0)
        text:SetPoint("RIGHT", row, "RIGHT", -4, 0)
        text:SetJustifyH("LEFT")
        text:SetText("")
        row.text = text
        row.glow = CreateOutlinePulseFrame(row, 8, 2)
        row._displayText = ""
        row._activeChallengeID = nil
        row._transition = nil
        row:SetScript("OnUpdate", function(_, elapsed)
            if not self or not self.frame or not self.frame:IsShown() then
                return
            end
            self:TickTrackerChallengeRow(i, elapsed)
        end)
        trackerChallengeRows[i] = row
    end

    local trackerHelperText = tracker:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    trackerHelperText:SetPoint("BOTTOM", tracker, "BOTTOM", 0, 8)
    trackerHelperText:SetJustifyH("CENTER")
    trackerHelperText:SetText("Type /mm, or click to open main panel")
    trackerHelperText:SetTextColor(0.78, 0.78, 0.78)

    local trackerMiniName = tracker:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    trackerMiniName:SetPoint("TOPLEFT", tracker, "TOPLEFT", 10, -10)
    trackerMiniName:SetJustifyH("LEFT")
    trackerMiniName:SetWidth(68)
    trackerMiniName:SetTextColor(1.0, 0.82, 0.1)
    trackerMiniName:SetText("")
    trackerMiniName:Hide()
    self:BindPetIdentityTooltip(trackerMiniName)

    local trackerMiniPanel = CreateFrame("Button", nil, tracker)
    trackerMiniPanel:SetPoint("TOPLEFT", tracker, "TOPLEFT", 8, -30)
    trackerMiniPanel:SetPoint("BOTTOMRIGHT", tracker, "BOTTOMRIGHT", -8, 8)
    trackerMiniPanel:RegisterForClicks("LeftButtonUp")
    trackerMiniPanel:SetScript("OnClick", function()
        self:ShowMainFrame()
    end)
    local trackerMiniBG = trackerMiniPanel:CreateTexture(nil, "BACKGROUND")
    trackerMiniBG:SetAllPoints(true)
    trackerMiniBG:SetColorTexture(0.08, 0.13, 0.18, 0.42)

    local function CreateMiniCard(parent, col, row, color)
        local cardW, cardH = 54, 38
        local gapX, gapY = 8, 8
        local x = 2 + (col * (cardW + gapX))
        local y = -2 - (row * (cardH + gapY))

        local card = CreateFrame("Frame", nil, parent)
        card:SetSize(cardW, cardH)
        card:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)

        local bg = card:CreateTexture(nil, "BACKGROUND")
        bg:SetAllPoints(true)
        bg:SetColorTexture(0.07, 0.09, 0.12, 0.95)

        local border = card:CreateTexture(nil, "BORDER")
        border:SetPoint("TOPLEFT", card, "TOPLEFT", 0, 0)
        border:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", 0, 0)
        border:SetColorTexture(0.16, 0.35, 0.45, 0.55)

        local value = card:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        value:SetPoint("CENTER", card, "CENTER", 0, 0)
        value:SetJustifyH("CENTER")
        value:SetText("0%")
        value:SetTextColor(color[1], color[2], color[3])
        return value
    end

    local trackerMiniHunger = CreateMiniCard(trackerMiniPanel, 0, 0, COLOR_HUNGER)
    local trackerMiniJoy = CreateMiniCard(trackerMiniPanel, 1, 0, COLOR_JOY)
    local trackerMiniEnergy = CreateMiniCard(trackerMiniPanel, 0, 1, COLOR_ENERGY)
    local trackerMiniClean = CreateMiniCard(trackerMiniPanel, 1, 1, COLOR_CLEAN)
    local trackerMiniEggCell = CreateFrame("Frame", nil, trackerMiniPanel)
    trackerMiniEggCell:SetSize(116, 84)
    trackerMiniEggCell:SetPoint("TOPLEFT", trackerMiniPanel, "TOPLEFT", 2, -2)
    local trackerMiniEggBG = trackerMiniEggCell:CreateTexture(nil, "BACKGROUND")
    trackerMiniEggBG:SetAllPoints(true)
    trackerMiniEggBG:SetColorTexture(0.07, 0.09, 0.12, 0.95)
    local trackerMiniEggBorder = trackerMiniEggCell:CreateTexture(nil, "BORDER")
    trackerMiniEggBorder:SetPoint("TOPLEFT", trackerMiniEggCell, "TOPLEFT", 0, 0)
    trackerMiniEggBorder:SetPoint("BOTTOMRIGHT", trackerMiniEggCell, "BOTTOMRIGHT", 0, 0)
    trackerMiniEggBorder:SetColorTexture(0.16, 0.35, 0.45, 0.55)
    local trackerMiniEggValue = trackerMiniEggCell:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    trackerMiniEggValue:SetPoint("CENTER", trackerMiniEggCell, "CENTER", 0, 0)
    trackerMiniEggValue:SetJustifyH("CENTER")
    trackerMiniEggValue:SetText("0%")
    trackerMiniEggValue:SetTextColor(COLOR_HATCH[1], COLOR_HATCH[2], COLOR_HATCH[3])
    trackerMiniEggCell:Hide()
    trackerMiniPanel:Hide()

    do
        local trackerThrottle = 0
        tracker:SetScript("OnUpdate", function(_, elapsed)
            trackerThrottle = trackerThrottle + (elapsed or 0)
            if trackerThrottle < 0.1 then
                return
            end
            trackerThrottle = 0
            if tracker:IsShown() then
                self:UpdateTrackerBasicButtonCooldowns()
            end
        end)
    end

    frame.SidePanel = sidePanel
    frame.TopPetInfoText = topPetInfoText
    frame.TopPetMetaText = topPetMetaText
    frame.CareAccordion = careAccordion
    frame.FeedActionButton = sidePanel.FeedAction
    frame.PlayActionButton = sidePanel.PlayAction
    frame.CleanActionButton = sidePanel.CleanAction
    frame.RestActionButton = sidePanel.RestAction
    frame.SootheCooldownBar = sootheCooldownBar
    frame.CleanEggCooldownBar = cleanCooldownBar
    frame.WarmCooldownBar = warmCooldownBar
    frame.WorkActionButton = sidePanel.WorkAction
    frame.IdentityText = identity
    frame.DetailsText = details
    frame.WorkStatusText = workStatus
    frame.EconomySummaryText = economySummary
    frame.HungerRow = hungerRow
    frame.JoyRow = joyRow
    frame.EnergyRow = energyRow
    frame.CleanRow = cleanRow
    frame.HatchNeedsRow = hatchNeedsRow
    frame.HatchNeedsButton = hatchNeedsButton
    frame.PortraitRestIcon = portraitRestIcon
    frame.QuickStoreFrame = storeFrame
    frame.QuickShopButton = quickShopButton
    frame.QuickShopButtonIcon = quickShopIcon
    frame.StoreShellsText = shellsText
    frame.StoreRows = storeFrame.rows
    frame.PetTexture = petTexture
    frame.EggModel = eggModel
    frame.HatchBar = nil
    frame.HatchText = nil
    frame.HatchReadyButton = nil
    frame.ChallengeCells = challengeCells
    frame.ChallengeText = nil
    frame.ChallengeRerollButton = nil
    frame.ChallengePanel = challengePanel
    frame.StoreDetailPanel = storeDetailPanel
    frame.StoreDetailRows = storeDetailRows
    frame.StoreDetailRowOrder = storeDetailRowOrder
    frame.StoreDetailScroll = storeDetailScroll
    frame.StoreSectionDropdown = sectionDropdown
    frame.StoreDetailSub = nil
    frame.StoreDetailShells = storeDetailShells
    frame.StoreDetailTotals = storeDetailTotals
    frame.CustomizationPanel = customizationPanel
    frame.CustomizationShellsText = customizationShellsText
    frame.CustomizationPageDropdown = customizationPageDropdown
    frame.CustomizationHabitatCards = customizationHabitatCards
    frame.TabPlaceholderPanel = tabPlaceholderPanel
    frame.TabPlaceholderTitle = tabPlaceholderTitle
    frame.TabPlaceholderBody = tabPlaceholderBody
    frame.CarePanel = carePanel
    frame.CareDescription = careBody
    frame.CareDescriptionScroll = careSummaryScroll
    frame.CareDescriptionScrollChild = careSummaryContent
    frame.InfoTelemetryButton = infoTelemetryButton
    frame.InfoRenameButton = renameButton
    frame.InfoPurchasesToggleButton = infoPurchasesToggleButton
    frame.InfoLossesToggleButton = infoLossesToggleButton
    frame.InfoActivityPanel = infoActivityPanel
    frame.InfoActivityText = infoActivityText
    frame.DevPanel = devPanel
    frame.DevDeleteButton = devDeleteButton
    frame.DevEggReadyButton = devEggReadyButton
    frame.DevMaxNeedsButton = devMaxNeedsButton
    frame.DevAddShellsButton = devAddShellsButton
    frame.DevClearCooldownsButton = devClearCooldownsButton
    frame.DevResetFirstUseButton = devResetFirstUseButton
    frame.DevRenameEditBox = devRenameEditBox
    frame.DevRenameApplyButton = devRenameApplyButton
    frame.DevPersonalityDropdown = devPersonalityDropdown
    frame.DevSecondaryPersonalityDropdown = devSecondaryPersonalityDropdown
    frame.DevApplyPersonalityButton = devApplyPersonalityButton
    frame.DevModelDropdown = devModelDropdown
    frame.DevApplyModelButton = devApplyModelButton
    frame.DevAutonomyDropdown = devAutonomyDropdown
    frame.DevTriggerAutonomyButton = devTriggerAutonomyButton
    frame.DevBgStageDropdown = devBgStageDropdown
    frame.DevBgChoiceDropdown = devBgChoiceDropdown
    frame.DevApplyBgButton = devApplyBgButton
    frame.DevChallengeCompleteChecks = devChallengeCompleteChecks
    frame.DevMurlocSoundsCount = devMurlocSoundsCount
    frame.DevMurlocSoundsScroll = devMurlocSoundsScroll
    frame.DevMurlocSoundsEmpty = devMurlocSoundsEmpty
    frame.DevMurlocSoundRows = devMurlocSoundRows
    frame.HabitatTitle = habitatTitle
    frame.HabitatBG = habitatBG
    frame.HabitatModelBG = habitatModelBG
    frame.HabitatShade = habitatShade
    frame.HabitatModelInput = habitatModelInput
    frame.HabitatResetModelButton = resetModelButton
    frame.HabitatResetModelIcon = resetModelIcon
    frame.ActivityLabel = nil
    frame.ActivityBox = activityBox
    frame.ActivityText = activityText
    frame.ChallengeLabel = nil
    frame.WelcomePanel = welcome
    frame.TrackerFrame = tracker
    frame.TrackerNameText = trackerName
    frame.TrackerShellsText = trackerShellsText
    frame.TrackerTitleText = trackerTitle
    frame.TrackerNeedDivider = trackerNeedDivider
    frame.TrackerNeedCells = trackerNeedCells
    frame.TrackerEggCell = trackerEggCell
    frame.TrackerEggBar = trackerEggBar
    frame.TrackerEggValue = trackerEggValue
    frame.TrackerChallengeRows = trackerChallengeRows
    frame.TrackerHelperText = trackerHelperText
    frame.TrackerChallengeDivider = trackerChallengeDivider
    frame.TrackerChallengeTitle = trackerChallengeTitle
    frame.TrackerScavengeStatus = trackerScavengeStatus
    frame.TrackerChallengeToggle = trackerChallengeToggle
    frame.TrackerBasicButtons = trackerBasicButtons
    frame.TrackerMiniModeButton = trackerMiniModeButton
    frame.TrackerHalfModeButton = trackerHalfModeButton
    frame.TrackerFullModeButton = trackerFullModeButton
    frame.TrackerMiniPanel = trackerMiniPanel
    frame.TrackerMiniNameText = trackerMiniName
    frame.TrackerMiniHungerText = trackerMiniHunger
    frame.TrackerMiniJoyText = trackerMiniJoy
    frame.TrackerMiniEnergyText = trackerMiniEnergy
    frame.TrackerMiniCleanText = trackerMiniClean
    frame.TrackerMiniEggCell = trackerMiniEggCell
    frame.TrackerMiniEggValue = trackerMiniEggValue

    self.frame = frame
    do
        local throttle = 0
        frame:SetScript("OnUpdate", function(_, elapsed)
            throttle = throttle + elapsed
            if throttle < 0.1 then
                return
            end
            throttle = 0
            if self.frame and self.frame:IsShown() then
                self:UpdateEggCooldownProgressBars()
                self:UpdateAdolescentCooldownProgressBars()
                self:UpdateAdolescentCooldownLabels()
                self:UpdateScavengeCountdown()
            end
        end)
    end
    self.activeTab = self.activeTab or "Habitat"
    self:UpdateDevTabVisibility()
    self:RefreshUI(true)
end

function MM:ResetMainFramePosition()
    if not self.frame then
        return
    end

    local profile = self.db.profile.frame
    profile.point = "CENTER"
    profile.x = 0
    profile.y = 0

    self.frame:ClearAllPoints()
    self.frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
end

function MM:ShowMainFrame()
    if not self.frame then
        self:InitializeUI()
    end
    self.frame:Show()
    self:RefreshUI(true)
end

function MM:HideMainFrame()
    if self.frame then
        self.frame:Hide()
        self:RefreshUI()
    end
end

function MM:ToggleMainFrame()
    if not self.frame or not self.frame:IsShown() then
        self:ShowMainFrame()
    else
        self:HideMainFrame()
    end
end

function MM:PlayHatchSequence()
    if not self.frame then
        return
    end

    local tex = self.frame.PetTexture
    tex:SetAlpha(0.25)
    C_Timer.After(0.25, function()
        if not self.frame then
            return
        end
        tex:SetTexture(ICON_MURLOC)
        tex:SetAlpha(1)
        self:RefreshUI()
    end)
end

function MM:EnsureAdolescentModel()
    if not self.frame or not self.frame.EggModel then
        return false
    end
    if self.frame._petModelResolved then
        return self.frame._petModelLoaded == true
    end

    local displayModel = self.frame.EggModel
    self.frame._petModelResolved = true
    self.frame._petModelLoaded = false
    self.frame._petModelSource = nil

    if self.frame.EggModel and self.frame.EggModel.ClearModel then
        pcall(self.frame.EggModel.ClearModel, self.frame.EggModel)
    end

    local pet = self:GetPet()
    self.db.profile.dev = self.db.profile.dev or {}
    local selectedDisplayID = tonumber(self.db.profile.dev.selectedHatchDisplayID)
    local activeDisplayID = selectedDisplayID or tonumber(pet and pet.modelDisplayID) or ADOLESCENT_DISPLAY_ID

    if displayModel.SetDisplayInfo then
        local ok = pcall(displayModel.SetDisplayInfo, displayModel, activeDisplayID)
        if ok then
            self.frame._petModelLoaded = true
            self.frame._petModelSource = "display:" .. tostring(activeDisplayID)
            pcall(displayModel.SetFacing, displayModel, ADOLESCENT_MODEL_BASE_FACING)
            pcall(displayModel.SetPosition, displayModel, ADOLESCENT_MODEL_BASE_X, ADOLESCENT_MODEL_BASE_Y, ADOLESCENT_MODEL_BASE_Z)
            pcall(displayModel.SetModelScale, displayModel, ADOLESCENT_MODEL_BASE_SCALE)
            pcall(displayModel.SetCamDistanceScale, displayModel, ADOLESCENT_MODEL_BASE_CAM)
        end
    end

    return self.frame._petModelLoaded
end

function MM:EnsureEggModel()
    if not self.frame or not self.frame.EggModel then
        return false
    end
    if self.frame._eggModelResolved then
        return self.frame._eggModelLoaded == true
    end

    local displayModel = self.frame.EggModel
    self.frame._eggModelResolved = true
    self.frame._eggModelLoaded = false
    self.frame._eggModelSource = nil

    if self.frame.EggModel and self.frame.EggModel.ClearModel then
        pcall(self.frame.EggModel.ClearModel, self.frame.EggModel)
    end

    if displayModel.SetDisplayInfo then
        local ok = pcall(displayModel.SetDisplayInfo, displayModel, EGG_DISPLAY_ID)
        if ok then
            self.frame._eggModelLoaded = true
            self.frame._eggModelSource = "display:" .. tostring(EGG_DISPLAY_ID)
            pcall(displayModel.SetFacing, displayModel, EGG_MODEL_BASE_FACING)
            pcall(displayModel.SetPosition, displayModel, EGG_MODEL_BASE_X, EGG_MODEL_BASE_Y, EGG_MODEL_BASE_Z)
            pcall(displayModel.SetModelScale, displayModel, EGG_MODEL_BASE_SCALE)
            pcall(displayModel.SetCamDistanceScale, displayModel, EGG_MODEL_BASE_CAM)
        end
    end

    return self.frame._eggModelLoaded
end

function MM:StopEggWiggle(preservePose)
    if not self.frame then
        return
    end
    if self.frame._eggWiggleTicker and self.frame._eggWiggleTicker.Cancel then
        self.frame._eggWiggleTicker:Cancel()
    end
    self.frame._eggWiggleTicker = nil
    self.frame._eggWiggleActive = false
    self.frame._eggWiggleToken = (self.frame._eggWiggleToken or 0) + 1

    if preservePose then
        return
    end
    if not self:GetPet() or self:GetPet().stage ~= "egg" then
        return
    end

    local model = self.frame.EggModel
    if model then
        if self:IsModelControlCustomized("egg") then
            self:ApplyModelControlState("egg")
        else
            pcall(model.SetFacing, model, EGG_MODEL_BASE_FACING)
            pcall(model.SetPosition, model, EGG_MODEL_BASE_X, EGG_MODEL_BASE_Y, EGG_MODEL_BASE_Z)
            pcall(model.SetModelScale, model, EGG_MODEL_BASE_SCALE)
            pcall(model.SetCamDistanceScale, model, EGG_MODEL_BASE_CAM)
        end
    end
end

function MM:StartEggWiggle()
    if not self.frame or not self.frame.EggModel then
        return
    end
    if self:IsModelControlCustomized("egg") then
        return
    end
    if self.frame._eggWiggleTicker then
        return
    end
    self.frame._eggWiggleActive = false
    self.frame._eggWiggleToken = self.frame._eggWiggleToken or 0

    self.frame._eggWiggleTicker = C_Timer.NewTicker(1.9, function()
        if not self.frame or not self.frame.EggModel then
            self:StopEggWiggle()
            return
        end
        if self:GetPet().stage ~= "egg" or not self.frame:IsShown() or self.activeTab ~= "Habitat" or not self.frame.EggModel:IsShown() then
            self:StopEggWiggle()
            return
        end
        if self.frame._eggWiggleActive then
            return
        end
        if math.random() < 0.35 then
            return
        end

        local model = self.frame.EggModel
        local token = (self.frame._eggWiggleToken or 0) + 1
        self.frame._eggWiggleToken = token
        self.frame._eggWiggleActive = true

        local dir = (math.random(0, 1) == 0) and -1 or 1
        pcall(model.SetFacing, model, EGG_MODEL_BASE_FACING + (math.rad(4.2) * dir))
        pcall(model.SetPosition, model, EGG_MODEL_BASE_X + (0.015 * dir), EGG_MODEL_BASE_Y, EGG_MODEL_BASE_Z)

        C_Timer.After(0.16, function()
            if not self.frame or self.frame._eggWiggleToken ~= token or self:GetPet().stage ~= "egg" then
                return
            end
            if not model then
                return
            end
            pcall(model.SetFacing, model, EGG_MODEL_BASE_FACING - (math.rad(2.1) * dir))
            pcall(model.SetPosition, model, EGG_MODEL_BASE_X - (0.008 * dir), EGG_MODEL_BASE_Y, EGG_MODEL_BASE_Z)
        end)

        C_Timer.After(0.34, function()
            if not self.frame or self.frame._eggWiggleToken ~= token then
                return
            end
            if model then
                pcall(model.SetFacing, model, EGG_MODEL_BASE_FACING)
                pcall(model.SetPosition, model, EGG_MODEL_BASE_X, EGG_MODEL_BASE_Y, EGG_MODEL_BASE_Z)
            end
            self.frame._eggWiggleActive = false
        end)
    end)
end

function MM:RefreshUI(force)
    if not self.frame then
        return
    end

    local pet = self:GetPet()
    local store = self:GetStore()
    local economy = self:GetEconomy()
    if self.CheckNeedThresholdMessages then
        self:CheckNeedThresholdMessages()
    end
    local activeTab = self.activeTab or "Habitat"
    local habitatActive = activeTab == "Habitat"
    local storeActive = activeTab == "Store"
    local customizationActive = activeTab == "Customization"
    local infoActive = activeTab == "Info"
    local devEnabled = self.db and self.db.profile and self.db.profile.dev and self.db.profile.dev.showDevTab == true
    local devActive = devEnabled and activeTab == "Dev"
    local resting = (IsResting and IsResting()) or false
    local frameScale = self:NormalizeMainFrameScale(self.db.profile.frame.scale)
    self.db.profile.frame.scale = frameScale
    self.frame:SetScale(frameScale)

    if self.frame.UpdateTopTabSelection then
        self.frame:UpdateTopTabSelection(activeTab)
    end
    if self.UpdateMinimapButtonVisibility then
        self:UpdateMinimapButtonVisibility()
    end

    if self.frame.PortraitRestIcon then
        if resting then
            self.frame.PortraitRestIcon:Show()
            if self.frame.PortraitRestIcon.Pulse and not self.frame.PortraitRestIcon.Pulse:IsPlaying() then
                self.frame.PortraitRestIcon.Pulse:Play()
            end
        else
            if self.frame.PortraitRestIcon.Pulse and self.frame.PortraitRestIcon.Pulse:IsPlaying() then
                self.frame.PortraitRestIcon.Pulse:Stop()
            end
            self.frame.PortraitRestIcon:Hide()
        end
    end

    if self.frame.TrackerFrame then
        self.db.profile.ui = self.db.profile.ui or {}
        local showTracker = self.db.profile.ui.showTracker ~= false and self.db.profile.welcomeSeen == true
        local trackerMode = tostring(self.db.profile.ui.trackerMode or "")
        if trackerMode ~= "mini" and trackerMode ~= "half" and trackerMode ~= "full" then
            trackerMode = (self.db.profile.ui.trackerChallengesCollapsed == true) and "half" or "full"
            self.db.profile.ui.trackerMode = trackerMode
        end
        local miniTracker = trackerMode == "mini"
        local collapsedChallenges = trackerMode == "half"
        local trackerScale = self:NormalizeTrackerScale(self.db.profile.ui.trackerScale)
        self.db.profile.ui.trackerChallengesCollapsed = collapsedChallenges
        self.db.profile.ui.trackerScale = trackerScale
        self.frame.TrackerFrame:SetShown(showTracker)
        self.frame.TrackerFrame:SetSize(miniTracker and 136 or 370, miniTracker and 124 or (collapsedChallenges and 160 or 270))
        self.frame.TrackerFrame:SetScale(trackerScale)
        if self.frame.TrackerMiniModeButton then
            self.frame.TrackerMiniModeButton:SetShown(not miniTracker)
        end
        if self.frame.TrackerHalfModeButton then
            self.frame.TrackerHalfModeButton:SetShown(miniTracker)
        end
        if self.frame.TrackerFullModeButton then
            self.frame.TrackerFullModeButton:SetShown(miniTracker)
        end
        if self.frame.TrackerNameText then
            self.frame.TrackerNameText:SetShown(not miniTracker)
        end
        if self.frame.TrackerTitleText then
            self.frame.TrackerTitleText:SetShown(false)
        end
        if self.frame.TrackerMiniNameText then
            self.frame.TrackerMiniNameText:SetShown(miniTracker)
        end
        if self.frame.TrackerShellsText then
            self.frame.TrackerShellsText:SetShown(not miniTracker)
        end
        if self.frame.TrackerNeedDivider then
            self.frame.TrackerNeedDivider:SetShown(not miniTracker)
        end
        if self.frame.TrackerNeedCells then
            for _, cell in pairs(self.frame.TrackerNeedCells) do
                if cell then
                    cell:SetShown((not miniTracker) and pet.stage ~= "egg")
                end
            end
        end
        if self.frame.TrackerEggCell then
            self.frame.TrackerEggCell:SetShown((not miniTracker) and pet.stage == "egg")
        end
        if self.frame.TrackerChallengeToggle then
            if collapsedChallenges then
                self.frame.TrackerChallengeToggle:SetNormalAtlas("ui-questtrackerbutton-expand-all", true)
                self.frame.TrackerChallengeToggle:SetPushedAtlas("ui-questtrackerbutton-expand-all-pressed", true)
            else
                self.frame.TrackerChallengeToggle:SetNormalAtlas("ui-questtrackerbutton-collapse-all", true)
                self.frame.TrackerChallengeToggle:SetPushedAtlas("ui-questtrackerbutton-collapse-all-pressed", true)
            end
            self.frame.TrackerChallengeToggle:SetHighlightAtlas("ui-questtrackerbutton-red-highlight", "ADD")
            self.frame.TrackerChallengeToggle:SetShown(not miniTracker)
        end
        if self.frame.TrackerChallengeDivider then
            self.frame.TrackerChallengeDivider:SetShown((not miniTracker) and (not collapsedChallenges))
        end
        if self.frame.TrackerChallengeTitle then
            self.frame.TrackerChallengeTitle:SetShown(not miniTracker)
            if collapsedChallenges then
                self.frame.TrackerChallengeTitle:SetText("Expand")
            else
                self.frame.TrackerChallengeTitle:SetText("Collapse")
            end
            self.frame.TrackerChallengeTitle:ClearAllPoints()
            if collapsedChallenges then
                self.frame.TrackerChallengeTitle:SetPoint("TOPLEFT", self.frame.TrackerFrame, "TOPLEFT", 10, -141)
            else
                self.frame.TrackerChallengeTitle:SetPoint("TOPLEFT", self.frame.TrackerFrame, "TOPLEFT", 10, -151)
            end
        end
        if self.frame.TrackerScavengeStatus then
            local isActive = (economy and economy.work and economy.work.active) == true
            if isActive then
                self.frame.TrackerScavengeStatus:SetText("|cff44dd66Scavenging: Active|r")
            else
                self.frame.TrackerScavengeStatus:SetText("|cff9aa3acScavenging: Inactive|r")
            end
            self.frame.TrackerScavengeStatus:ClearAllPoints()
            if collapsedChallenges then
                self.frame.TrackerScavengeStatus:SetPoint("TOPRIGHT", self.frame.TrackerFrame, "TOPRIGHT", -10, -141)
            else
                self.frame.TrackerScavengeStatus:SetPoint("TOPRIGHT", self.frame.TrackerFrame, "TOPRIGHT", -10, -151)
            end
            self.frame.TrackerScavengeStatus:SetShown(not miniTracker)
        end
        if self.frame.TrackerHelperText then
            self.frame.TrackerHelperText:SetShown((not miniTracker) and (not collapsedChallenges))
        end
        if self.frame.TrackerBasicButtons then
            local showActionButtons = (not miniTracker) and (not collapsedChallenges)
            local eggStage = pet.stage == "egg"
            for _, btn in pairs(self.frame.TrackerBasicButtons) do
                local show = showActionButtons and ((eggStage and btn._mmEggAction ~= nil) or ((not eggStage) and btn._mmSection ~= nil))
                btn:SetShown(show)
                if show then
                    local c = btn._mmColor or COLOR_WHITE
                    local fs = btn:GetFontString()
                    if fs then
                        fs:SetTextColor(c[1], c[2], c[3])
                    end
                end
            end
            self:UpdateTrackerBasicButtonCooldowns()
        end

        if self.frame.TrackerNameText then
            local trackerName = self:GetPetDisplayName()
            if pet and pet.stage == "egg" then
                trackerName = string.format("%s (%d%%)", trackerName, math.floor((pet.hatchProgress or 0) + 0.5))
            end
            self.frame.TrackerNameText:SetText(trackerName)
            if self.frame.TrackerMiniNameText then
                self.frame.TrackerMiniNameText:SetText(self:GetPetDisplayName())
            end
        end
        if self.frame.TrackerShellsText then
            self.frame.TrackerShellsText:SetText(ColorizeShellText(string.format("Shells: %d", tonumber(store and store.shells) or 0)))
        end

        if self.frame.TrackerNeedCells then
            local needMap = {
                hunger = pet.hunger,
                joy = pet.joy,
                energy = pet.energy,
                cleanliness = pet.cleanliness,
            }
            for needKey, value in pairs(needMap) do
                local cell = self.frame.TrackerNeedCells[needKey]
                if cell and cell.bar and cell.value then
                    local v = tonumber(value) or 0
                    cell.bar:SetValue(v)
                    cell.value:SetText(string.format("%d%%", math.floor(v + 0.5)))
                    cell:SetShown((not miniTracker) and pet.stage ~= "egg")
                end
            end
        end
        if self.frame.TrackerEggCell and self.frame.TrackerEggBar and self.frame.TrackerEggValue then
            local hatchValue = tonumber(pet.hatchProgress) or 0
            self.frame.TrackerEggCell:SetShown((not miniTracker) and pet.stage == "egg")
            self.frame.TrackerEggBar:SetValue(hatchValue)
            self.frame.TrackerEggValue:SetText(string.format("%d%%", math.floor(hatchValue + 0.5)))
        end

        if self.frame.TrackerChallengeRows then
            local challenges = (economy and economy.challenges and economy.challenges.active) or {}
            local eggStage = pet and pet.stage == "egg"
            for i = 1, 4 do
                local row = self.frame.TrackerChallengeRows[i]
                if row and row.text then
                    row:SetShown((not miniTracker) and (not collapsedChallenges))
                    local challenge = challenges[i]
                    local nextChallengeID = challenge and tostring(challenge.id or "") or ""
                    local nextText = self:BuildTrackerChallengeRowText(challenge, eggStage)
                    if row._activeChallengeID and row._activeChallengeID ~= nextChallengeID then
                        self:BeginTrackerChallengeRowTransition(row, nextChallengeID, nextText)
                    elseif not row._transition then
                        row._activeChallengeID = nextChallengeID
                        row._displayText = nextText
                        row.text:SetText(nextText)
                        row.text:SetAlpha(1)
                        if row.glow then
                            row.glow:SetAlpha(0)
                        end
                    end
                end
            end
        end

        if self.frame.TrackerMiniPanel then
            self.frame.TrackerMiniPanel:SetShown(miniTracker)
        end
        local miniEggStage = pet.stage == "egg"
        if self.frame.TrackerMiniEggCell then
            self.frame.TrackerMiniEggCell:SetShown(miniTracker and miniEggStage)
        end
        if self.frame.TrackerMiniEggValue then
            local hatchValue = math.floor((tonumber(pet.hatchProgress) or 0) + 0.5)
            self.frame.TrackerMiniEggValue:SetText(string.format("%d%%", hatchValue))
        end
        if self.frame.TrackerMiniHungerText then
            local card = self.frame.TrackerMiniHungerText:GetParent()
            if card then
                card:SetShown(not miniEggStage)
            end
            self.frame.TrackerMiniHungerText:SetText(string.format("%d%%", math.floor((tonumber(pet.hunger) or 0) + 0.5)))
        end
        if self.frame.TrackerMiniJoyText then
            local card = self.frame.TrackerMiniJoyText:GetParent()
            if card then
                card:SetShown(not miniEggStage)
            end
            self.frame.TrackerMiniJoyText:SetText(string.format("%d%%", math.floor((tonumber(pet.joy) or 0) + 0.5)))
        end
        if self.frame.TrackerMiniEnergyText then
            local card = self.frame.TrackerMiniEnergyText:GetParent()
            if card then
                card:SetShown(not miniEggStage)
            end
            self.frame.TrackerMiniEnergyText:SetText(string.format("%d%%", math.floor((tonumber(pet.energy) or 0) + 0.5)))
        end
        if self.frame.TrackerMiniCleanText then
            local card = self.frame.TrackerMiniCleanText:GetParent()
            if card then
                card:SetShown(not miniEggStage)
            end
            self.frame.TrackerMiniCleanText:SetText(string.format("%d%%", math.floor((tonumber(pet.cleanliness) or 0) + 0.5)))
        end
    end

    self.db.profile.dev = self.db.profile.dev or {}
    self.db.profile.dev.stageBackgroundOverrides = self.db.profile.dev.stageBackgroundOverrides or {}
    local activeStageForBackground = pet.stage == "egg" and "egg" or "adolescent"
    local activeBackgroundChoice = self.db.profile.dev.stageBackgroundOverrides[activeStageForBackground] or "auto"
    local customizationChoice = self.GetActiveHabitatBackgroundChoice and self:GetActiveHabitatBackgroundChoice() or "auto"
    if DEV_BACKGROUND_CHOICES_BY_KEY[customizationChoice] and customizationChoice ~= "auto" then
        activeBackgroundChoice = customizationChoice
    end
    if not DEV_BACKGROUND_CHOICES_BY_KEY[activeBackgroundChoice] then
        activeBackgroundChoice = "auto"
        self.db.profile.dev.stageBackgroundOverrides[activeStageForBackground] = activeBackgroundChoice
    end
    if self.db.profile.dev.selectedBackgroundStage ~= "egg" and self.db.profile.dev.selectedBackgroundStage ~= "adolescent" then
        self.db.profile.dev.selectedBackgroundStage = activeStageForBackground
    end
    do
        local selectedStage = self.db.profile.dev.selectedBackgroundStage or activeStageForBackground
        local selectedChoice = self.db.profile.dev.selectedBackgroundChoice
            or self.db.profile.dev.stageBackgroundOverrides[selectedStage]
            or "auto"
        if not DEV_BACKGROUND_CHOICES_BY_KEY[selectedChoice] then
            selectedChoice = "auto"
        end
        self.db.profile.dev.selectedBackgroundChoice = selectedChoice
    end
    ApplyHabitatBackgroundByChoice(self.frame.HabitatBG, self.frame.HabitatModelBG, self.frame.HabitatShade, activeBackgroundChoice)
    local traitDisplay = self.GetTraitDisplayText and self:GetTraitDisplayText(pet) or (pet.personality or "Unknown")

    if self.frame.TopPetInfoText then
        self.frame.TopPetInfoText:SetText(self:GetPetDisplayName())
        if pet.stage ~= "egg" and self.GetMurlocRarityColor then
            local r, g, b = self:GetMurlocRarityColor(pet.modelRarity)
            self.frame.TopPetInfoText:SetTextColor(r, g, b)
        else
            self.frame.TopPetInfoText:SetTextColor(1.0, 0.82, 0.1)
        end
    end
    if self.frame.TopPetMetaText then
        if pet.stage == "egg" then
            self.frame.TopPetMetaText:SetText("")
        else
            self.frame.TopPetMetaText:SetText(string.format("%s %s", traitDisplay, pet.stage))
        end
    end
    if self.frame.IdentityText then
        self.frame.IdentityText:SetText("")
        self.frame.IdentityText:Hide()
    end
    if self.frame.DetailsText then
        self.frame.DetailsText:SetText("")
        self.frame.DetailsText:Hide()
    end
    self._lastWorkStatusText = nil
    self._lastWorkButtonText = nil
    self:UpdateScavengeCountdown()
    do
        local ticks = (economy.work and tonumber(economy.work.totalTicks)) or 0
        local shellsFound = (economy.work and tonumber(economy.work.totalEarned)) or 0
        local pendingSkips = (economy.work and tonumber(economy.work.pendingSkippedPayoutTicks)) or 0
        local restartRemaining = (self.GetWorkStartCooldownRemaining and self:GetWorkStartCooldownRemaining()) or 0
        local payoutPerTick = (self.GetCurrentScavengeTickPayout and self:GetCurrentScavengeTickPayout()) or 0
        local payoutMultiplierPct = (self.GetWorkPayoutMultiplier and math.floor(((self:GetWorkPayoutMultiplier() - 1) * 100) + 0.5)) or 0
        local payoutFlatBonus = (self.GetWorkPayoutFlatBonus and self:GetWorkPayoutFlatBonus()) or 0
        local gatherBonusRemaining = (self.GetGatheringScavengeBonusRemaining and self:GetGatheringScavengeBonusRemaining()) or 0
        local gatherBonusLine = "Timed Gather Buff: inactive"
        if gatherBonusRemaining > 0 then
            local mins = math.floor(gatherBonusRemaining / 60)
            local secs = math.floor(gatherBonusRemaining % 60)
            gatherBonusLine = string.format("Timed Gather Buff: active (%d:%02d)", mins, secs)
        end

        if pet.stage == "egg" then
            self.frame.EconomySummaryText:SetText(
                "Status: Locked (egg stage)\n"
                .. "Shells/Tick: Locked\n"
                .. "Shells Found: 0\n"
                .. "Ticks Scavenging: 0\n"
                .. "Rejoin: Locked"
            )
        else
            local rejoinText = restartRemaining > 0 and (math.ceil(restartRemaining) .. "s") or "Ready"
            self.frame.EconomySummaryText:SetText(string.format(
                "Shells/Tick: %d\nScavenge Bonus: +%d%%, +%d flat\n%s\nShells Found: %d\nTicks Scavenging: %d\nPayout Skips: %d\nRejoin: %s",
                payoutPerTick,
                payoutMultiplierPct,
                payoutFlatBonus,
                gatherBonusLine,
                shellsFound,
                ticks,
                pendingSkips,
                rejoinText
            ))
        end
    end

    local function updateRow(row, value)
        row.bar:SetValue(value)
        row.valueText:SetText(string.format("%d%%", math.floor(value + 0.5)))
    end

    updateRow(self.frame.HungerRow, pet.hunger)
    updateRow(self.frame.JoyRow, pet.joy)
    updateRow(self.frame.EnergyRow, pet.energy)
    updateRow(self.frame.CleanRow, pet.cleanliness)
    if self.frame.HatchNeedsRow then
        updateRow(self.frame.HatchNeedsRow, pet.hatchProgress or 0)
    end

    local eggGreyAlpha = (pet.stage == "egg") and 0.45 or 1.0
    if self.frame.HungerRow then self.frame.HungerRow:SetShown(pet.stage ~= "egg") end
    if self.frame.JoyRow then self.frame.JoyRow:SetShown(pet.stage ~= "egg") end
    if self.frame.EnergyRow then self.frame.EnergyRow:SetShown(pet.stage ~= "egg") end
    if self.frame.CleanRow then self.frame.CleanRow:SetShown(pet.stage ~= "egg") end
    if self.frame.HatchNeedsRow then self.frame.HatchNeedsRow:SetShown(pet.stage == "egg") end
    if self.frame.HatchNeedsButton then
        local hatchReady = (tonumber(pet.hatchProgress) or 0) >= 100
        self.frame.HatchNeedsButton:SetShown(pet.stage == "egg")
        self.frame.HatchNeedsButton:SetEnabled(hatchReady)
        self.frame.HatchNeedsButton:SetText(hatchReady and "Hatch Me!" or "Hatch Me")
    end
    if self.frame.QuickStoreFrame then self.frame.QuickStoreFrame:SetAlpha(eggGreyAlpha) end

    if pet.stage == "egg" then
        self.frame.StoreShellsText:SetText(ColorizeShellText("Shells: Locked"))
    else
        self.frame.StoreShellsText:SetText(ColorizeShellText(string.format("Shells: %d", store.shells)))
    end

    for key, row in pairs(self.frame.StoreRows or {}) do
        if row.itemSource == "food" then
            local foodItem
            for _, item in ipairs(self:GetFoodItems()) do
                if item.key == key then
                    foodItem = item
                    break
                end
            end
            if foodItem then
                local owned = tonumber(store.inventory[key]) or 0
                row.owned:SetText(tostring(owned))
                if row.effect then
                    local tooltipData = self:BuildStoreTooltipData("food", key, "feed")
                    local preferredNeed = GetPrimaryNeedKeyForTooltipSource("food", "feed")
                    row.effect:SetText(BuildPrimaryNeedEffectText(tooltipData and tooltipData.deltas, preferredNeed))
                end
                row.buy:SetText(ColorizeShellText(string.format("%d Shells", foodItem.price)))
                row.buy:SetEnabled((pet.stage ~= "egg") and (store.shells >= foodItem.price))
            end
        else
            local entry = self:GetStoreEntry(key)
            local owned = self:GetStoreItemCount(key)
            if entry then
                local isCareItem = entry.category == "play" or entry.category == "rest" or entry.category == "clean"
                if entry.category == "play" or entry.category == "rest" or entry.category == "clean" then
                    row.owned:SetText("")
                else
                    row.owned:SetText(tostring(owned))
                end
                if row.effect then
                    if entry.category == "play" then
                        local tooltipData = self:BuildStoreTooltipData("store", key)
                        local preferredNeed = GetPrimaryNeedKeyForTooltipSource("store", nil, entry.category)
                        row.effect:SetText(BuildPrimaryNeedEffectText(tooltipData and tooltipData.deltas, preferredNeed))
                    elseif entry.category == "rest" then
                        local tooltipData = self:BuildStoreTooltipData("store", key)
                        local preferredNeed = GetPrimaryNeedKeyForTooltipSource("store", nil, entry.category)
                        row.effect:SetText(BuildPrimaryNeedEffectText(tooltipData and tooltipData.deltas, preferredNeed))
                    elseif entry.category == "clean" then
                        local tooltipData = self:BuildStoreTooltipData("store", key)
                        local preferredNeed = GetPrimaryNeedKeyForTooltipSource("store", nil, entry.category)
                        row.effect:SetText(BuildPrimaryNeedEffectText(tooltipData and tooltipData.deltas, preferredNeed))
                    else
                        row.effect:SetText("")
                    end
                end
                local maxOwned = tonumber(entry.maxOwned)
                local isOwnedUpgrade = isCareItem and ((maxOwned and maxOwned > 0 and owned >= maxOwned) or ((not maxOwned) and owned > 0))
                local effectivePrice = (self.GetStoreEntryEffectivePrice and self:GetStoreEntryEffectivePrice(entry)) or tonumber(entry.price) or 0
                if isOwnedUpgrade then
                    row.buy:SetText("Owned")
                    row.buy:SetEnabled(false)
                else
                    row.buy:SetText(ColorizeShellText(string.format("%d Shells", effectivePrice)))
                    row.buy:SetEnabled((pet.stage ~= "egg") and self:CanBuyStoreEntry(entry))
                end
            end
        end
    end

    if self.frame.CareAccordion then
        local accordion = self.frame.CareAccordion
        local isEggStage = pet.stage == "egg"
        accordion:SetShown(not isEggStage)

        if not isEggStage then
            self.db.profile.ui = self.db.profile.ui or {}
            local openSection = self.db.profile.ui.openCareSection or "feed"
            local sectionCategoryMap = {
                play = "play",
                rest = "rest",
                clean = "clean",
            }
            local sectionBonusKeyMap = {
                play = "joyBonus",
                rest = "restBonus",
                clean = "cleanBonus",
            }

            local tabStartY = 0
            local tabWidth = 66
            local tabHeight = 22
            local tabGapX = 4
            local tabGapY = 4
            local listTopOffset = 52
            local listOffset = listTopOffset
            for tabIndex, sectionKey in ipairs(accordion.order or {}) do
                local section = accordion.sections and accordion.sections[sectionKey]
                if section and section.header then
                    section.header:Show()
                    section.header:ClearAllPoints()
                    section.header:SetSize(tabWidth, tabHeight)
                    local col = (tabIndex - 1) % 2
                    local row = math.floor((tabIndex - 1) / 2)
                    section.header:SetPoint("TOPLEFT", accordion, "TOPLEFT", col * (tabWidth + tabGapX), -(tabStartY + row * (tabHeight + tabGapY)))

                    local expanded = openSection == sectionKey
                    local remaining = 0
                    if self.GetAdolescentActionCooldown then
                        remaining = self:GetAdolescentActionCooldown(sectionKey)
                    end
                    section.header:SetEnabled(true)
                    if section.header._selectedTexture then
                        if expanded then
                            section.header._selectedTexture:Show()
                        else
                            section.header._selectedTexture:Hide()
                        end
                    end
                    if section.header.Left and section.header.Middle and section.header.Right then
                        local r, g, b = GetCareSectionButtonColors(sectionKey, expanded)
                        section.header.Left:SetVertexColor(r, g, b)
                        section.header.Middle:SetVertexColor(r, g, b)
                        section.header.Right:SetVertexColor(r, g, b)
                    end
                    ApplyCareSectionTextColor(section.header, sectionKey, expanded)
                    if remaining > 0 then
                        section.header:SetText(string.format("%s %.1f", section.label, remaining))
                    else
                        section.header:SetText(section.label)
                    end

                    local entries = {}
                    if sectionKey == "feed" then
                        local basicFeedValue = BASIC_ACTION_UI_VALUE
                        entries[#entries + 1] = {
                            itemKey = "__free__",
                            label = "Basic Feed",
                            count = 1,
                            itemIsFree = true,
                        }
                        local foodItems = {}
                        for _, item in ipairs(self:GetFoodItems()) do
                            foodItems[#foodItems + 1] = item
                        end
                        table.sort(foodItems, function(a, b)
                            local ah = tonumber(a.hunger) or 0
                            local bh = tonumber(b.hunger) or 0
                            if ah ~= bh then
                                return ah < bh
                            end
                            local ap = tonumber(a.price) or 0
                            local bp = tonumber(b.price) or 0
                            if ap ~= bp then
                                return ap < bp
                            end
                            return tostring(a.label or "") < tostring(b.label or "")
                        end)
                        for _, item in ipairs(foodItems) do
                            entries[#entries + 1] = {
                                itemKey = item.key,
                                label = item.label,
                                count = tonumber(store.inventory[item.key]) or 0,
                                itemIsFree = false,
                            }
                        end
                    else
                        local basicValue = BASIC_ACTION_UI_VALUE
                        if sectionKey == "play" then
                            local playMultiplier = self:HasStoreUpgrade("playBell") and 1.10 or 1.0
                            basicValue = math.floor((BASIC_ACTION_UI_VALUE * playMultiplier) + 0.5)
                        elseif sectionKey == "rest" then
                            local restMultiplier = self:HasStoreUpgrade("napWhistle") and 1.10 or 1.0
                            basicValue = math.floor((BASIC_ACTION_UI_VALUE * restMultiplier) + 0.5)
                        end

                        local basicLabel = "Basic Action"
                        if sectionKey == "play" then
                            basicLabel = "Basic Play"
                        elseif sectionKey == "rest" then
                            basicLabel = "Basic Rest"
                        elseif sectionKey == "clean" then
                            basicLabel = "Basic Clean"
                        end
                        entries[#entries + 1] = {
                            itemKey = "__free__",
                            label = basicLabel,
                            count = 1,
                            itemIsFree = true,
                        }
                        local category = sectionCategoryMap[sectionKey]
                        local bonusKey = sectionBonusKeyMap[sectionKey]
                        local sectionItems = {}
                        for _, entry in ipairs(self:GetStoreCatalog()) do
                            if entry.category == category then
                                sectionItems[#sectionItems + 1] = entry
                            end
                        end
                        table.sort(sectionItems, function(a, b)
                            local av = bonusKey and (tonumber(a[bonusKey]) or 0) or 0
                            local bv = bonusKey and (tonumber(b[bonusKey]) or 0) or 0
                            if av ~= bv then
                                return av < bv
                            end
                            local ap = tonumber(a.price) or 0
                            local bp = tonumber(b.price) or 0
                            if ap ~= bp then
                                return ap < bp
                            end
                            return tostring(a.label or "") < tostring(b.label or "")
                        end)
                        for _, entry in ipairs(sectionItems) do
                            entries[#entries + 1] = {
                                itemKey = entry.key,
                                label = entry.label,
                                count = self:GetStoreItemCount(entry.key),
                                itemIsFree = false,
                            }
                        end
                    end

                    for i, row in ipairs(section.rows or {}) do
                        if expanded and entries[i] then
                            local data = entries[i]
                            row.itemKey = data.itemKey
                            row.itemIsFree = data.itemIsFree == true
                            row.itemCount = data.count
                            local rowColor = GetNeedColorByCategory(sectionKey)
                            local mainGain = 0
                            if sectionKey == "feed" then
                                if row.itemIsFree then
                                    local foodMultiplier = self:GetFoodEffectMultiplier()
                                    local hungerFlat = self:HasStoreUpgrade("tideLadle") and 4 or 0
                                    mainGain = math.floor((BASIC_ACTION_UI_VALUE * foodMultiplier) + hungerFlat + 0.5)
                                else
                                    local foodItem = nil
                                    for _, item in ipairs(self:GetFoodItems()) do
                                        if item.key == data.itemKey then
                                            foodItem = item
                                            break
                                        end
                                    end
                                    if foodItem then
                                        local foodMultiplier = self:GetFoodEffectMultiplier()
                                        local hungerFlat = self:HasStoreUpgrade("tideLadle") and 4 or 0
                                        mainGain = math.floor(((foodItem.hunger or 0) * foodMultiplier) + hungerFlat + 0.5)
                                    end
                                end
                            elseif sectionKey == "play" then
                                local playMultiplier = self:HasStoreUpgrade("playBell") and 1.10 or 1.0
                                if row.itemIsFree then
                                    mainGain = math.floor((BASIC_ACTION_UI_VALUE * playMultiplier) + 0.5)
                                else
                                    local playEntry = self:GetStoreEntry(data.itemKey)
                                    mainGain = math.floor(((tonumber(playEntry and playEntry.joyBonus) or BASIC_ACTION_UI_VALUE) * playMultiplier) + 0.5)
                                end
                            elseif sectionKey == "rest" then
                                local restMultiplier = self:HasStoreUpgrade("napWhistle") and 1.10 or 1.0
                                if row.itemIsFree then
                                    mainGain = math.floor((BASIC_ACTION_UI_VALUE * restMultiplier) + 0.5)
                                else
                                    local itemMultiplier = self:HasStoreUpgrade("cozyNest") and 1.20 or 1.0
                                    local restEntry = self:GetStoreEntry(data.itemKey)
                                    mainGain = math.floor(((tonumber(restEntry and restEntry.restBonus) or BASIC_ACTION_UI_VALUE) * itemMultiplier * restMultiplier) + 0.5)
                                end
                            elseif sectionKey == "clean" then
                                if row.itemIsFree then
                                    local cleanMultiplier = self:HasStoreUpgrade("washStation") and 1.20 or 1.0
                                    mainGain = math.floor((BASIC_ACTION_UI_VALUE * cleanMultiplier) + 0.5)
                                else
                                    local cleanMultiplier = self:HasStoreUpgrade("washStation") and 1.20 or 1.0
                                    local cleanEntry = self:GetStoreEntry(data.itemKey)
                                    mainGain = math.floor(((tonumber(cleanEntry and cleanEntry.cleanBonus) or BASIC_ACTION_UI_VALUE) * cleanMultiplier) + 0.5)
                                end
                            end

                            if row.ownedText then
                                if row.itemIsFree or sectionKey ~= "feed" then
                                    row.ownedText:SetText("")
                                else
                                    row.ownedText:SetText(tostring(data.count or 0))
                                end
                                row.ownedText:SetTextColor(COLOR_SHELL[1], COLOR_SHELL[2], COLOR_SHELL[3])
                            end
                            if row.nameText then
                                row.nameText:SetText(data.label or "")
                                row.nameText:SetTextColor(rowColor[1], rowColor[2], rowColor[3])
                            end
                            if row.effectText then
                                local tooltipSource = "store"
                                if row.itemIsFree then
                                    tooltipSource = "basic"
                                elseif sectionKey == "feed" then
                                    tooltipSource = "food"
                                end
                                local tooltipData = self:BuildStoreTooltipData(tooltipSource, data.itemKey, sectionKey)
                                local preferredNeed = GetPrimaryNeedKeyForTooltipSource(
                                    tooltipSource,
                                    sectionKey,
                                    (tooltipSource == "store") and sectionKey or nil
                                )
                                row.effectText:SetText(BuildPrimaryNeedEffectText(tooltipData and tooltipData.deltas, preferredNeed))
                                row.effectText:SetTextColor(1, 1, 1)
                            end
                            row:SetText("")
                            local canUse = row.itemIsFree or data.count > 0
                            row:SetEnabled(canUse and remaining <= 0)
                            row:ClearAllPoints()
                            row:SetPoint("TOPLEFT", accordion, "TOPLEFT", -2, -listOffset)
                            row:Show()
                            listOffset = listOffset + 21
                        else
                            row.itemKey = nil
                            row.itemIsFree = false
                            row.itemCount = 0
                            if row.ownedText then row.ownedText:SetText("") end
                            if row.nameText then row.nameText:SetText("") end
                            if row.effectText then row.effectText:SetText("") end
                            row:SetText("")
                            row:Hide()
                        end
                    end
                end
            end
        else
            for _, section in pairs(accordion.sections or {}) do
                if section.header then
                    section.header:Hide()
                end
                if section.cooldownBar then
                    section.cooldownBar:Hide()
                end
                for _, row in ipairs(section.rows or {}) do
                    row.itemKey = nil
                    row.itemIsFree = false
                    row.itemCount = 0
                    if row.ownedText then row.ownedText:SetText("") end
                    if row.nameText then row.nameText:SetText("") end
                    if row.effectText then row.effectText:SetText("") end
                    row:SetText("")
                    row:Hide()
                end
            end
        end
    end

    if self.frame.ChallengeCells then
        for i = 1, 4 do
            self:RefreshChallengeCard(i)
        end
    end

    if self.frame.HabitatBG then
        local bgActive = self.frame.HabitatBG._mmBackgroundActive ~= false
        self.frame.HabitatBG:SetShown(habitatActive and bgActive)
    end
    if self.frame.HabitatModelBG then
        local modelActive = self.frame.HabitatModelBG._mmBackgroundActive == true
        self.frame.HabitatModelBG:SetShown(habitatActive and modelActive)
    end
    if self.frame.HabitatShade then self.frame.HabitatShade:SetShown(habitatActive) end
    if self.frame.HabitatTitle then
        if habitatActive then
            self.frame.HabitatTitle:SetText("Murloc Habitat")
        else
            self.frame.HabitatTitle:SetText(activeTab)
        end
    end
    if self.frame.PetTexture then self.frame.PetTexture:SetShown(habitatActive) end
    if self.frame.ActivityLabel then self.frame.ActivityLabel:SetShown(true) end
    if self.frame.ActivityBox then self.frame.ActivityBox:SetShown(true) end
    if self.frame.ChallengeLabel then self.frame.ChallengeLabel:SetShown(true) end
    if self.frame.ChallengePanel then self.frame.ChallengePanel:SetShown(true) end
    if self.frame.StoreDetailPanel then self.frame.StoreDetailPanel:SetShown(storeActive) end
    if self.frame.CustomizationPanel then self.frame.CustomizationPanel:SetShown(customizationActive) end
    if self.frame.CarePanel then self.frame.CarePanel:SetShown(infoActive) end
    if self.frame.DevPanel then self.frame.DevPanel:SetShown(devActive) end
    if self.frame.TabPlaceholderPanel then
        self.frame.TabPlaceholderPanel:SetShown((not habitatActive) and (not storeActive) and (not customizationActive) and (not infoActive) and (not devActive))
        if self.frame.TabPlaceholderTitle then
            self.frame.TabPlaceholderTitle:SetText(activeTab)
        end
        if self.frame.TabPlaceholderBody then
            self.frame.TabPlaceholderBody:SetText(activeTab .. " content is planned for a future update.")
        end
    end
    if self.frame.CareDescription then
        if infoActive then
            self.db.profile.ui = self.db.profile.ui or {}
            local expandPurchases = self.db.profile.ui.infoExpandPurchases == true
            local expandLosses = self.db.profile.ui.infoExpandLosses == true
            local petName = TrimString(pet.name or "")
            local challengeTotal = 0
            local challengeDone = 0
            if economy and economy.challenges and economy.challenges.active then
                challengeTotal = #economy.challenges.active
                for _, ch in ipairs(economy.challenges.active) do
                    if ch.progress >= ch.goal then
                        challengeDone = challengeDone + 1
                    end
                end
            end
            local shellsEarned = tonumber(economy.shellsEarned) or 0
            local shellsSpent = tonumber(economy.shellsSpent) or 0
            local scavengeTicks = (economy.work and tonumber(economy.work.totalTicks)) or 0
            local scavengeFound = (economy.work and tonumber(economy.work.totalEarned)) or 0
            local stats = self.GetInteractionStats and self:GetInteractionStats() or {}
            local actions = stats.actions or {}
            local scavengeStats = stats.scavenge or {}
            local challengeStats = stats.challenges or {}
            local purchasesByKey = stats.purchasesByKey or {}
            local lossesByKey = stats.lossesByKey or {}
            local purchasesTotal = tonumber(stats.purchasesTotal) or 0
            local lossesTotal = tonumber(stats.lossesTotal) or 0
            local lifetimeLogCount = #((self.db and self.db.profile and self.db.profile.activityHistory) or {})

            local infoRenderKeyParts = {
                tostring(petName),
                tostring(pet.stage or ""),
                tostring(math.floor((pet.hatchProgress or 0) + 0.5)),
                tostring(math.floor(pet.ageMinutes or 0)),
                tostring(math.floor((pet.hunger or 0) + 0.5)),
                tostring(math.floor((pet.joy or 0) + 0.5)),
                tostring(math.floor((pet.energy or 0) + 0.5)),
                tostring(math.floor((pet.cleanliness or 0) + 0.5)),
                tostring(store.shells or 0),
                tostring(shellsEarned),
                tostring(shellsSpent),
                tostring(scavengeFound),
                tostring(scavengeTicks),
                tostring(tonumber(scavengeStats.starts) or 0),
                tostring(tonumber(scavengeStats.manualRecalls) or 0),
                tostring(tonumber(scavengeStats.autoPauses) or 0),
                tostring(tonumber(scavengeStats.autoResumes) or 0),
                tostring(challengeDone),
                tostring(challengeTotal),
                tostring(tonumber(challengeStats.reloads) or 0),
                tostring(lifetimeLogCount),
                tostring(tonumber(actions.feed) or 0),
                tostring(tonumber(actions.play) or 0),
                tostring(tonumber(actions.rest) or 0),
                tostring(tonumber(actions.clean) or 0),
                tostring(tonumber(actions.eggCare) or 0),
                tostring(tonumber(actions.find) or 0),
                tostring(tonumber(actions.autonomous) or 0),
                tostring(purchasesTotal),
                tostring(lossesTotal),
                expandPurchases and "p1" or "p0",
                expandLosses and "l1" or "l0",
            }
            if expandPurchases then
                infoRenderKeyParts[#infoRenderKeyParts + 1] = BuildPositiveCountSignature(purchasesByKey)
            end
            if expandLosses then
                infoRenderKeyParts[#infoRenderKeyParts + 1] = BuildPositiveCountSignature(lossesByKey)
            end
            local infoRenderKey = table.concat(infoRenderKeyParts, "|")

            if force or self._infoSummaryRenderKey ~= infoRenderKey then
                local function getItemLabel(itemKey)
                    for _, food in ipairs(self:GetFoodItems() or {}) do
                        if food.key == itemKey then
                            return food.label
                        end
                    end
                    local entry = self:GetStoreEntry(itemKey)
                    if entry and entry.label then
                        return entry.label
                    end
                    return tostring(itemKey or "Unknown")
                end

                local function sortedBreakdownLines(byKeyTable)
                    local rows = {}
                    for itemKey, amount in pairs(byKeyTable or {}) do
                        local value = tonumber(amount) or 0
                        if value > 0 then
                            rows[#rows + 1] = {
                                key = itemKey,
                                amount = value,
                                label = getItemLabel(itemKey),
                            }
                        end
                    end
                    table.sort(rows, function(a, b)
                        if a.amount == b.amount then
                            return tostring(a.label) < tostring(b.label)
                        end
                        return a.amount > b.amount
                    end)

                    local lines = {}
                    for _, row in ipairs(rows) do
                        lines[#lines + 1] = string.format("  - %s x%d", row.label, row.amount)
                    end
                    return lines
                end

                local maturityLine = pet.stage == "egg"
                    and string.format("Hatch Progress: %d%%", math.floor((pet.hatchProgress or 0) + 0.5))
                    or string.format("Age Ticks: %d", math.floor(pet.ageMinutes or 0))
                local shellNet = shellsEarned - shellsSpent
                local infoLines = {
                    string.format("Name: %s", petName ~= "" and petName or "Unhatched Egg"),
                    string.format("Stage: %s", pet.stage),
                    string.format("Traits: %s", traitDisplay),
                    maturityLine,
                    string.format("Needs: H %d%%  J %d%%  E %d%%  C %d%%",
                        math.floor((pet.hunger or 0) + 0.5),
                        math.floor((pet.joy or 0) + 0.5),
                        math.floor((pet.energy or 0) + 0.5),
                        math.floor((pet.cleanliness or 0) + 0.5)
                    ),
                    string.format("Shells: %d", store.shells or 0),
                    string.format("Shells Earned: %d", shellsEarned),
                    string.format("Shells Spent: %d", shellsSpent),
                    string.format("Shell Net: %d", shellNet),
                    string.format("Scavenge Shells Found: %d", scavengeFound),
                    string.format("Scavenge Ticks: %d", scavengeTicks),
                    string.format("Scavenge Starts: %d", tonumber(scavengeStats.starts) or 0),
                    string.format("Scavenge Manual Recalls: %d", tonumber(scavengeStats.manualRecalls) or 0),
                    string.format("Scavenge Auto Pauses/Resumes: %d / %d", tonumber(scavengeStats.autoPauses) or 0, tonumber(scavengeStats.autoResumes) or 0),
                    string.format("Challenges: %d/%d complete", challengeDone, challengeTotal),
                    string.format("Challenge Reloads: %d", tonumber(challengeStats.reloads) or 0),
                    string.format("Lifetime Activity Lines: %d", lifetimeLogCount),
                    "",
                    string.format("Care Actions: Feed %d | Play %d | Rest %d | Clean %d | Egg Care %d | Find %d | Auto %d",
                        tonumber(actions.feed) or 0,
                        tonumber(actions.play) or 0,
                        tonumber(actions.rest) or 0,
                        tonumber(actions.clean) or 0,
                        tonumber(actions.eggCare) or 0,
                        tonumber(actions.find) or 0,
                        tonumber(actions.autonomous) or 0
                    ),
                    string.format("Items Purchased: %d", purchasesTotal),
                    string.format("Items Lost/Broken: %d", lossesTotal),
                }

                if expandPurchases then
                    infoLines[#infoLines + 1] = "Purchased Breakdown:"
                    local rows = sortedBreakdownLines(purchasesByKey)
                    if #rows == 0 then
                        infoLines[#infoLines + 1] = "  - None yet"
                    else
                        for _, line in ipairs(rows) do
                            infoLines[#infoLines + 1] = line
                        end
                    end
                end

                if expandLosses then
                    infoLines[#infoLines + 1] = "Loss/Broken Breakdown:"
                    local rows = sortedBreakdownLines(lossesByKey)
                    if #rows == 0 then
                        infoLines[#infoLines + 1] = "  - None yet"
                    else
                        for _, line in ipairs(rows) do
                            infoLines[#infoLines + 1] = line
                        end
                    end
                end
                self.frame.CareDescription:SetText(table.concat(infoLines, "\n"))
                if self.frame.CareDescriptionScrollChild then
                    local contentHeight = math.max(math.ceil((self.frame.CareDescription:GetStringHeight() or 0) + 8), 1)
                    self.frame.CareDescriptionScrollChild:SetHeight(contentHeight)
                end
                self._infoSummaryRenderKey = infoRenderKey
            end
        else
            self._infoSummaryRenderKey = nil
        end
    end
    if self.frame.InfoPurchasesToggleButton then
        self.frame.InfoPurchasesToggleButton:SetShown(infoActive)
        self.frame.InfoPurchasesToggleButton:SetText((self.db.profile.ui and self.db.profile.ui.infoExpandPurchases) and "Purchases -" or "Purchases +")
    end
    if self.frame.InfoLossesToggleButton then
        self.frame.InfoLossesToggleButton:SetShown(infoActive)
        self.frame.InfoLossesToggleButton:SetText((self.db.profile.ui and self.db.profile.ui.infoExpandLosses) and "Losses -" or "Losses +")
    end
    if self.frame.InfoRenameButton then
        self.frame.InfoRenameButton:SetShown(false)
    end
    if self.frame.InfoActivityPanel then
        self.frame.InfoActivityPanel:SetShown(infoActive)
    end
    if self.frame.InfoActivityText then
        if infoActive then
            local log = (self.db and self.db.profile and self.db.profile.activityHistory) or {}
            if #log == 0 then
                log = (self.db and self.db.profile and self.db.profile.activityLog) or {}
            end

            local total = #log
            local lastLine = ""
            if total > 0 then
                lastLine = GetActivityEntryLine(self, log[total])
            end
            local renderKey = tostring(total) .. "\31" .. tostring(lastLine or "")
            if force or self._infoActivityRenderKey ~= renderKey then
                self.frame.InfoActivityText:Clear()
                local firstIndex = math.max(1, total - ACTIVITY_HISTORY_RENDER_LIMIT + 1)
                for i = firstIndex, total do
                    local entry = log[i]
                    local line = GetActivityEntryLine(self, entry)
                    if line ~= "" then
                        local color = GetActivityLineColor(self, entry)
                        self.frame.InfoActivityText:AddMessage(line, color[1], color[2], color[3])
                    end
                end
                self.frame.InfoActivityText:ScrollToBottom()
                self._infoActivityRenderKey = renderKey
            end
        else
            self._infoActivityRenderKey = nil
        end
    end

    if devEnabled and devActive then
        if self.frame.DevRenameEditBox and not self.frame.DevRenameEditBox:HasFocus() then
            local currentName = TrimString((pet and pet.name) or "")
            self.frame.DevRenameEditBox:SetText(currentName)
        end

        if self.frame.DevPersonalityDropdown then
            local selectedPersonality = self.db.profile.dev.selectedPersonality or pet.personality or "Unknown"
            if self.frame.DevPersonalityDropdown.OverrideText then
                self.frame.DevPersonalityDropdown:OverrideText(selectedPersonality)
            else
                UIDropDownMenu_SetSelectedValue(self.frame.DevPersonalityDropdown, selectedPersonality)
                UIDropDownMenu_SetText(self.frame.DevPersonalityDropdown, selectedPersonality)
            end
        end
        if self.frame.DevSecondaryPersonalityDropdown then
            local selectedSecondary = self.db.profile.dev.selectedSecondaryPersonality or pet.personalitySecondary or "Unknown"
            if self.frame.DevSecondaryPersonalityDropdown.OverrideText then
                self.frame.DevSecondaryPersonalityDropdown:OverrideText(selectedSecondary)
            else
                UIDropDownMenu_SetSelectedValue(self.frame.DevSecondaryPersonalityDropdown, selectedSecondary)
                UIDropDownMenu_SetText(self.frame.DevSecondaryPersonalityDropdown, selectedSecondary)
            end
        end
        if self.frame.DevModelDropdown then
            local selectedDisplayID = tonumber(self.db.profile.dev.selectedHatchDisplayID)
            local selectedText = "Weighted Random"
            if selectedDisplayID and self.GetMurlocModelByDisplayID then
                local entry = self:GetMurlocModelByDisplayID(selectedDisplayID)
                if entry then
                    local rarityLabel = self.GetMurlocRarityLabel and self:GetMurlocRarityLabel(entry.rarity) or (entry.rarity or "Common")
                    selectedText = string.format("%s - %s (%d)", rarityLabel, entry.label or "Display", selectedDisplayID)
                else
                    selectedText = string.format("Display %d", selectedDisplayID)
                end
            end
            if self.frame.DevModelDropdown.OverrideText then
                self.frame.DevModelDropdown:OverrideText(selectedText)
            else
                UIDropDownMenu_SetSelectedValue(self.frame.DevModelDropdown, selectedDisplayID or "__random__")
                UIDropDownMenu_SetText(self.frame.DevModelDropdown, selectedText)
            end
        end
        if self.frame.DevAutonomyDropdown then
            local autonomyRenderKey = table.concat({
                tostring(pet.personality or ""),
                tostring(pet.personalitySecondary or ""),
                tostring(math.floor((pet.hunger or 0) + 0.5)),
                tostring(math.floor((pet.joy or 0) + 0.5)),
                tostring(math.floor((pet.energy or 0) + 0.5)),
                tostring(math.floor((pet.cleanliness or 0) + 0.5)),
                tostring((economy.work and economy.work.active) and 1 or 0),
                tostring((economy.work and tonumber(economy.work.currentShiftTicks)) or 0),
                tostring((pet.behavior and pet.behavior.lastEventKey) or ""),
                tostring((pet.behavior and pet.behavior.nextDecisionTick) or 0),
            }, "|")
            local options = nil
            if force or self._devAutonomyOptionsKey ~= autonomyRenderKey or type(self._devAutonomyOptionsCache) ~= "table" then
                options = self.GetAutonomousBehaviorOptions and self:GetAutonomousBehaviorOptions(true) or {}
                self._devAutonomyOptionsCache = options
                self._devAutonomyOptionsKey = autonomyRenderKey
            else
                options = self._devAutonomyOptionsCache
            end
            local selectedKey = self.db.profile.dev.selectedAutonomyEventKey
            local selectedText = nil

            if #options > 0 then
                for _, option in ipairs(options) do
                    if option.key == selectedKey then
                        selectedText = string.format("%s (%.2f%%)", option.label or option.key or "Unknown", (option.probability or 0) * 100)
                        break
                    end
                end
                if not selectedText then
                    selectedKey = options[1].key
                    self.db.profile.dev.selectedAutonomyEventKey = selectedKey
                    selectedText = string.format("%s (%.2f%%)", options[1].label or options[1].key or "Unknown", (options[1].probability or 0) * 100)
                end
                if self.frame.DevAutonomyDropdown.OverrideText then
                    self.frame.DevAutonomyDropdown:OverrideText(selectedText)
                else
                    UIDropDownMenu_SetSelectedValue(self.frame.DevAutonomyDropdown, selectedKey)
                    UIDropDownMenu_SetText(self.frame.DevAutonomyDropdown, selectedText)
                end
            else
                self.db.profile.dev.selectedAutonomyEventKey = nil
                if self.frame.DevAutonomyDropdown.OverrideText then
                    self.frame.DevAutonomyDropdown:OverrideText("No autonomous actions available")
                else
                    UIDropDownMenu_SetSelectedValue(self.frame.DevAutonomyDropdown, nil)
                    UIDropDownMenu_SetText(self.frame.DevAutonomyDropdown, "No autonomous actions available")
                end
            end

            if self.frame.DevTriggerAutonomyButton then
                self.frame.DevTriggerAutonomyButton:SetEnabled(#options > 0)
            end
        end
        if self.frame.DevBgStageDropdown then
            local selectedStage = self.db.profile.dev.selectedBackgroundStage or activeStageForBackground
            self.db.profile.dev.selectedBackgroundStage = selectedStage
            local selectedStageText = selectedStage == "egg" and "Egg" or "Adolescent+"
            if self.frame.DevBgStageDropdown.OverrideText then
                self.frame.DevBgStageDropdown:OverrideText(selectedStageText)
            else
                UIDropDownMenu_SetSelectedValue(self.frame.DevBgStageDropdown, selectedStage)
                UIDropDownMenu_SetText(self.frame.DevBgStageDropdown, selectedStageText)
            end
        end
        if self.frame.DevBgChoiceDropdown then
            local selectedStage = self.db.profile.dev.selectedBackgroundStage or activeStageForBackground
            local selectedChoice = self.db.profile.dev.selectedBackgroundChoice
                or self.db.profile.dev.stageBackgroundOverrides[selectedStage]
                or "auto"
            self.db.profile.dev.selectedBackgroundChoice = selectedChoice
            local bgText = GetDevBackgroundLabel(selectedChoice)
            if self.frame.DevBgChoiceDropdown.OverrideText then
                self.frame.DevBgChoiceDropdown:OverrideText(bgText)
            else
                UIDropDownMenu_SetSelectedValue(self.frame.DevBgChoiceDropdown, selectedChoice)
                UIDropDownMenu_SetText(self.frame.DevBgChoiceDropdown, bgText)
            end
        end
        if self.frame.DevChallengeCompleteChecks then
            local activeChallenges = (economy and economy.challenges and economy.challenges.active) or {}
            for i, check in ipairs(self.frame.DevChallengeCompleteChecks) do
                if check then
                    check:SetChecked(false)
                    check:SetEnabled(activeChallenges[i] ~= nil)
                end
            end
        end
        if self.frame.DevMurlocSoundsScroll and self.frame.DevMurlocSoundRows then
            local sounds = GetDevMurlocSoundEntries()
            local totalSounds = #sounds
            local visibleRows = #self.frame.DevMurlocSoundRows
            if FauxScrollFrame_Update then
                FauxScrollFrame_Update(self.frame.DevMurlocSoundsScroll, totalSounds, visibleRows, DEV_MURLOC_SOUND_ROW_HEIGHT)
            end

            local offset = 0
            if FauxScrollFrame_GetOffset then
                offset = FauxScrollFrame_GetOffset(self.frame.DevMurlocSoundsScroll) or 0
            end
            local maxOffset = math.max(0, totalSounds - visibleRows)
            if offset > maxOffset then
                offset = maxOffset
                if FauxScrollFrame_SetOffset then
                    FauxScrollFrame_SetOffset(self.frame.DevMurlocSoundsScroll, offset)
                    if FauxScrollFrame_Update then
                        FauxScrollFrame_Update(self.frame.DevMurlocSoundsScroll, totalSounds, visibleRows, DEV_MURLOC_SOUND_ROW_HEIGHT)
                    end
                end
            end

            for i, row in ipairs(self.frame.DevMurlocSoundRows) do
                local entry = sounds[i + offset]
                if entry then
                    row:Show()
                    row.IndexText:SetText(string.format("%d.", entry.index))
                    row.LabelText:SetText(string.format("%s (%d)", tostring(entry.display or entry.key or "Unknown"), entry.id))
                    row.PlayButton._mmSoundEntry = entry
                    row.PlayButton:SetEnabled(true)
                else
                    row:Hide()
                    row.PlayButton._mmSoundEntry = nil
                    row.PlayButton:SetEnabled(false)
                end
            end

            if self.frame.DevMurlocSoundsCount then
                self.frame.DevMurlocSoundsCount:SetText(string.format("%d total", totalSounds))
            end
            if self.frame.DevMurlocSoundsEmpty then
                self.frame.DevMurlocSoundsEmpty:SetShown(totalSounds == 0)
            end
        end
    else
        self._devAutonomyOptionsCache = nil
        self._devAutonomyOptionsKey = nil
    end
    if self.frame.StoreDetailRows then
        if storeActive then
            local selectedSection = store.selectedSection
            if selectedSection == "feeding" then
                selectedSection = "habitat"
                store.selectedSection = "habitat"
            end
            selectedSection = selectedSection or "food"
            local selectedSectionLabel = self:GetStoreSectionLabel(selectedSection)
            if self.frame.StoreSectionDropdown then
                if self.frame.StoreSectionDropdown.OverrideText then
                    self.frame.StoreSectionDropdown:OverrideText(selectedSectionLabel)
                else
                    UIDropDownMenu_SetSelectedValue(self.frame.StoreSectionDropdown, selectedSection)
                    UIDropDownMenu_SetText(self.frame.StoreSectionDropdown, selectedSectionLabel)
                end
            end
            if self.frame.StoreDetailShells then
                self.frame.StoreDetailShells:SetText(ColorizeShellText(string.format("Shells: %d", tonumber(store.shells) or 0)))
            end
            local totalsText = nil
            if self.GetStoreSectionTotalsText then
                totalsText = self:GetStoreSectionTotalsText(selectedSection)
            end
            if self.IsNameTagFirstPurchaseFree and self:IsNameTagFirstPurchaseFree() then
                local freeNameTagLine = ColorizeLabel("First Nametag use is FREE (Trinkets).", COLOR_HATCH)
                if totalsText and totalsText ~= "" then
                    totalsText = totalsText .. "\n" .. freeNameTagLine
                else
                    totalsText = freeNameTagLine
                end
            end
            if self.frame.StoreDetailTotals then
                if totalsText and totalsText ~= "" then
                    self.frame.StoreDetailTotals:SetText(totalsText)
                    self.frame.StoreDetailTotals:Show()
                else
                    self.frame.StoreDetailTotals:SetText("")
                    self.frame.StoreDetailTotals:Hide()
                end
            end

            local sectionEntries = GetCachedStoreSectionEntries(self, selectedSection)
            local shouldPushOwnedToBottom = (selectedSection == "trinket" or selectedSection == "habitat")

            store.storeScrollOffsets = store.storeScrollOffsets or {}
            local maxVisibleRows = 8
            local maxOffset = math.max(0, #sectionEntries - maxVisibleRows)
            local currentOffset = tonumber(store.storeScrollOffsets[selectedSection]) or 0
            currentOffset = self:Clamp(math.floor(currentOffset + 0.5), 0, maxOffset)
            store.storeScrollOffsets[selectedSection] = currentOffset

            if self.frame.StoreDetailScroll then
                self.frame.StoreDetailScroll._maxOffset = maxOffset
                self.frame.StoreDetailScroll:SetMinMaxValues(0, maxOffset)
                self.frame.StoreDetailScroll:SetValueStep(1)
                self.frame.StoreDetailScroll:SetValue(currentOffset)
                self.frame.StoreDetailScroll:SetShown(maxOffset > 0)
            end

            local detailRenderKeyParts = {
                tostring(selectedSection),
                tostring(currentOffset),
                tostring(pet.stage or ""),
                tostring(store.shells or 0),
                tostring(totalsText or ""),
            }
            for _, entry in ipairs(sectionEntries) do
                if entry.source == "food" then
                    detailRenderKeyParts[#detailRenderKeyParts + 1] = string.format("%s:%d", tostring(entry.key or ""), tonumber(store.inventory[entry.key]) or 0)
                else
                    detailRenderKeyParts[#detailRenderKeyParts + 1] = string.format("%s:%d", tostring(entry.key or ""), tonumber(self:GetStoreItemCount(entry.key)) or 0)
                end
            end
            local detailRenderKey = table.concat(detailRenderKeyParts, "|")

            local listTopOffset = -66
            if self.frame.StoreDetailTotals and self.frame.StoreDetailTotals:IsShown() then
                listTopOffset = -106
            end
            if self.frame.StoreDetailScroll then
                local panelHeight = self.frame.StoreDetailPanel:GetHeight() or 458
                local topInset = math.abs(listTopOffset)
                local visibleListSpan = 302
                local bottomOffset = math.max(10, panelHeight - topInset - visibleListSpan)
                self.frame.StoreDetailScroll:ClearAllPoints()
                self.frame.StoreDetailScroll:SetPoint("TOPRIGHT", self.frame.StoreDetailPanel, "TOPRIGHT", -8, listTopOffset)
                self.frame.StoreDetailScroll:SetPoint("BOTTOMRIGHT", self.frame.StoreDetailPanel, "BOTTOMRIGHT", -8, bottomOffset)
            end

            if force or self._storeDetailRenderKey ~= detailRenderKey then
                for _, row in ipairs(self.frame.StoreDetailRowOrder or {}) do
                    row:Hide()
                end

                local visibleIndex = 0
                local startIndex = currentOffset + 1
                local endIndex = math.min(#sectionEntries, startIndex + maxVisibleRows - 1)
                for listIndex = startIndex, endIndex do
                    local entry = sectionEntries[listIndex]
                    local rowKey = (entry.source == "food") and ("food:" .. entry.key) or entry.key
                    local row = self.frame.StoreDetailRows[rowKey]
                    if row then
                        visibleIndex = visibleIndex + 1
                        row:ClearAllPoints()
                        row:SetPoint("TOPLEFT", self.frame.StoreDetailPanel, "TOPLEFT", 8, listTopOffset - ((visibleIndex - 1) * 38))
                        row:Show()
                        if row.FreeHighlight then
                            row.FreeHighlight:Hide()
                        end
                        row.Icon:SetTexture(entry.icon)
                        row.Title:SetText(entry.label)
                        local entryColor = COLOR_WHITE
                        if entry.source == "food" then
                            entryColor = GetNeedColorByCategory("food")
                        elseif entry.source == "store" and entry._entry then
                            entryColor = GetNeedColorByCategory(entry._entry.category)
                        end

                        if entry.source == "food" then
                            local owned = tonumber(store.inventory[entry.key]) or 0
                            row.Icon:SetDesaturated(false)
                            row.Title:SetTextColor(unpack(entryColor))
                            row.Desc:SetTextColor(1, 1, 1)
                            row.Owned:SetTextColor(1, 1, 1)
                            local tooltipData = self:BuildStoreTooltipData("food", entry.key, "feed")
                            local effectText = BuildStoreNeedEffectDescription("Consumable: ", tooltipData and tooltipData.deltas, tooltipData and tooltipData.traitDeltas)
                            row.Desc:SetText(effectText or "Consumable")
                            row.Owned:SetText(string.format("Number Owned: %d", owned))
                            if pet.stage == "egg" then
                                row.Buy:SetText("Locked")
                                row.Buy:SetEnabled(false)
                            else
                                row.Buy:SetText(ColorizeShellText(string.format("%d Shells", entry.price)))
                                row.Buy:SetEnabled((store.shells or 0) >= (entry.price or 0))
                            end
                        else
                            local realEntry = entry._entry
                            local nameTagFree = realEntry and realEntry.key == "nameTag" and self.IsNameTagFirstPurchaseFree and self:IsNameTagFirstPurchaseFree()
                            local effectivePrice = (self.GetStoreEntryEffectivePrice and self:GetStoreEntryEffectivePrice(realEntry)) or (tonumber(entry.price) or 0)
                            local owned = self:GetStoreItemCount(entry.key)
                            local canBuy = self:CanBuyStoreEntry(realEntry)
                            local isCareItem = realEntry and (realEntry.category == "play" or realEntry.category == "rest" or realEntry.category == "clean")
                            local isOwnedUpgrade = (entry.maxOwned == 1 and owned > 0 and (shouldPushOwnedToBottom or isCareItem))
                            if realEntry and realEntry.category == "play" then
                                local tooltipData = self:BuildStoreTooltipData("store", entry.key)
                                local effectText = BuildStoreNeedEffectDescription("Use effect: ", tooltipData and tooltipData.deltas, tooltipData and tooltipData.traitDeltas)
                                row.Desc:SetText(effectText or "Use effect")
                            elseif realEntry and realEntry.category == "rest" then
                                local tooltipData = self:BuildStoreTooltipData("store", entry.key)
                                local effectText = BuildStoreNeedEffectDescription("Use effect: ", tooltipData and tooltipData.deltas, tooltipData and tooltipData.traitDeltas)
                                row.Desc:SetText(effectText or "Use effect")
                            elseif realEntry and realEntry.category == "clean" then
                                local tooltipData = self:BuildStoreTooltipData("store", entry.key)
                                local effectText = BuildStoreNeedEffectDescription("Use effect: ", tooltipData and tooltipData.deltas, tooltipData and tooltipData.traitDeltas)
                                row.Desc:SetText(effectText or "Use effect")
                            else
                                row.Desc:SetText(FormatNeedEffectsInDescription(entry.description))
                            end
                            if isCareItem then
                                row.Owned:SetText("")
                            elseif entry.maxOwned and entry.maxOwned == 1 then
                                row.Owned:SetText(owned > 0 and "Owned" or "Not owned")
                            else
                                row.Owned:SetText(string.format("Owned: %d", owned))
                            end
                            if nameTagFree then
                                row.Title:SetText(entry.label .. " (FREE)")
                            else
                                row.Title:SetText(entry.label)
                            end
                            if pet.stage == "egg" then
                                row.Buy:SetText("Locked")
                                row.Buy:SetEnabled(false)
                            elseif isOwnedUpgrade then
                                row.Buy:SetText("Owned")
                                row.Buy:SetEnabled(false)
                            else
                                if nameTagFree then
                                    row.Buy:SetText(ColorizeLabel("FREE", COLOR_HATCH))
                                else
                                    row.Buy:SetText(ColorizeShellText(string.format("%d Shells", effectivePrice)))
                                end
                                row.Buy:SetEnabled(canBuy)
                            end

                            if isOwnedUpgrade then
                                row.Icon:SetDesaturated(true)
                                row.Title:SetTextColor(0.62, 0.62, 0.62)
                                row.Desc:SetTextColor(0.58, 0.58, 0.58)
                                row.Owned:SetTextColor(0.70, 0.70, 0.70)
                            else
                                row.Icon:SetDesaturated(false)
                                if nameTagFree then
                                    row.Title:SetTextColor(COLOR_HATCH[1], COLOR_HATCH[2], COLOR_HATCH[3])
                                else
                                    row.Title:SetTextColor(unpack(entryColor))
                                end
                                row.Desc:SetTextColor(1, 1, 1)
                                row.Owned:SetTextColor(1, 1, 1)
                            end
                            if nameTagFree and row.FreeHighlight then
                                row.FreeHighlight:Show()
                            end
                        end
                    end
                end
                self._storeDetailRenderKey = detailRenderKey
            end
        else
            self._storeDetailRenderKey = nil
        end
    end

    if self.frame.CustomizationShellsText then
        if pet.stage == "egg" then
            self.frame.CustomizationShellsText:SetText(ColorizeShellText("Shells: Locked"))
        else
            self.frame.CustomizationShellsText:SetText(ColorizeShellText(string.format("Shells: %d", tonumber(store.shells) or 0)))
        end
    end
    if self.frame.CustomizationPageDropdown then
        local customizationState = self.GetHabitatCustomizationState and self:GetHabitatCustomizationState() or {}
        local selectedPage = tostring(customizationState.selectedPage or "habitats")
        if selectedPage == "" then
            selectedPage = "habitats"
            customizationState.selectedPage = selectedPage
        end
        if self.frame.CustomizationPageDropdown.OverrideText then
            self.frame.CustomizationPageDropdown:OverrideText("Habitats")
        else
            UIDropDownMenu_SetSelectedValue(self.frame.CustomizationPageDropdown, selectedPage)
            UIDropDownMenu_SetText(self.frame.CustomizationPageDropdown, "Habitats")
        end
    end
    if self.frame.CustomizationHabitatCards and self.GetHabitatCustomizationCatalog then
        local options = self:GetHabitatCustomizationCatalog() or {}
        local activeChoice = self.GetActiveHabitatBackgroundChoice and self:GetActiveHabitatBackgroundChoice() or "auto"
        for i, card in ipairs(self.frame.CustomizationHabitatCards) do
            local option = options[i]
            if not option then
                card:Hide()
            else
                card:Show()
                local price = tonumber(option.price) or 0
                local owned = self.IsHabitatBackgroundOwned and self:IsHabitatBackgroundOwned(option.key)
                local isActive = tostring(activeChoice or "auto") == tostring(option.key or "")
                local canAfford = (tonumber(store.shells) or 0) >= price

                card.Title:SetText(option.label or "Habitat")
                if isActive then
                    card.Title:SetTextColor(COLOR_HATCH[1], COLOR_HATCH[2], COLOR_HATCH[3])
                elseif owned then
                    card.Title:SetTextColor(0.78, 0.82, 0.9)
                else
                    card.Title:SetTextColor(1.0, 1.0, 1.0)
                end

                if price <= 0 then
                    card.Price:SetText(ColorizeLabel("FREE", COLOR_HATCH))
                elseif owned then
                    card.Price:SetText(ColorizeLabel(string.format("Owned (%d shells)", price), COLOR_SHELL))
                else
                    card.Price:SetText(ColorizeShellText(string.format("%d Shells", price)))
                end

                if card.Selected then
                    card.Selected:SetShown(isActive)
                end
                if card.Preview then
                    ApplyHabitatBackgroundPreviewByChoice(card.Preview, option.key)
                end

                if pet.stage == "egg" and not owned then
                    card.Buy:SetText("Locked")
                    card.Buy:SetEnabled(false)
                    card:SetAlpha(0.72)
                elseif owned then
                    if isActive then
                        card.Buy:SetText("Applied")
                        card.Buy:SetEnabled(false)
                    else
                        card.Buy:SetText("Apply")
                        card.Buy:SetEnabled(true)
                    end
                    card:SetAlpha(1.0)
                else
                    card.Buy:SetText("Buy")
                    card.Buy:SetEnabled(canAfford)
                    card:SetAlpha(1.0)
                end
            end
        end
    end

    if pet.stage == "egg" then
        if self.frame.FeedActionButton then
            self.frame.FeedActionButton:Show()
            self.frame.FeedActionButton:SetText("Cannot Feed Egg")
            self.frame.FeedActionButton:SetEnabled(false)
        end
        if self.frame.PlayActionButton then
            self.frame.PlayActionButton:Show()
            self.frame.PlayActionButton:SetText("Soothe Egg")
        end
        if self.frame.CleanActionButton then
            self.frame.CleanActionButton:Show()
            self.frame.CleanActionButton:SetText("Clean Egg")
        end
        if self.frame.RestActionButton then
            self.frame.RestActionButton:Show()
            self.frame.RestActionButton:SetText("Warm Egg")
        end
        self:UpdateEggCooldownProgressBars()
        self:UpdateAdolescentCooldownProgressBars()
        self.frame.WorkActionButton:Show()
        self.frame.WorkActionButton:SetEnabled(false)
        self.frame.WorkActionButton:SetText("Too Young")
        self.frame.WorkActionButton:ClearAllPoints()
        self.frame.WorkActionButton:SetPoint("TOPLEFT", self.frame.SidePanel, "TOPLEFT", 166, -198)
        if self.frame.WorkStatusText then
            self.frame.WorkStatusText:ClearAllPoints()
            self.frame.WorkStatusText:SetPoint("TOPLEFT", self.frame.WorkActionButton, "BOTTOMLEFT", 0, -8)
        end
        if self.frame.EconomySummaryText then
            self.frame.EconomySummaryText:ClearAllPoints()
            self.frame.EconomySummaryText:SetPoint("TOPLEFT", self.frame.WorkStatusText, "BOTTOMLEFT", 0, -8)
            self.frame.EconomySummaryText:SetWidth(154)
        end
    else
        if self.frame.FeedActionButton then
            self.frame.FeedActionButton:Hide()
        end
        if self.frame.PlayActionButton then
            self.frame.PlayActionButton:Hide()
        end
        if self.frame.CleanActionButton then
            self.frame.CleanActionButton:Hide()
        end
        if self.frame.RestActionButton then
            self.frame.RestActionButton:Hide()
        end
        self:UpdateEggCooldownProgressBars()
        self:UpdateAdolescentCooldownProgressBars()
        self.frame.WorkActionButton:Show()
        self.frame.WorkActionButton:SetEnabled(true)
        self.frame.WorkActionButton:ClearAllPoints()
        self.frame.WorkActionButton:SetPoint("TOPLEFT", self.frame.SidePanel, "TOPLEFT", 166, -198)
        if self.frame.WorkStatusText then
            self.frame.WorkStatusText:ClearAllPoints()
            self.frame.WorkStatusText:SetPoint("TOPLEFT", self.frame.WorkActionButton, "BOTTOMLEFT", 0, -8)
        end
        if self.frame.EconomySummaryText then
            self.frame.EconomySummaryText:ClearAllPoints()
            self.frame.EconomySummaryText:SetPoint("TOPLEFT", self.frame.WorkStatusText, "BOTTOMLEFT", 0, -8)
            self.frame.EconomySummaryText:SetWidth(154)
        end
        self:UpdateScavengeCountdown()
    end

    if self.frame.ActivityText then
        self.frame.ActivityText:Clear()
        local log = (self.db and self.db.profile and self.db.profile.activityLog) or {}
        for _, entry in ipairs(log) do
            local line = GetActivityEntryLine(self, entry)
            if line ~= "" then
                local color = GetActivityLineColor(self, entry)
                self.frame.ActivityText:AddMessage(line, color[1], color[2], color[3])
            end
        end
        self.frame.ActivityText:ScrollToBottom()
    end

    if habitatActive then
        if pet.stage == "egg" then
            local hasEggModel = self:EnsureEggModel()
            if hasEggModel and self.frame.EggModel and self.frame:IsShown() then
                self.frame.PetTexture:Hide()
                self.frame.EggModel:Show()
                self:ApplyModelControlState("egg")
                if self:IsModelControlCustomized("egg") then
                    self:StopEggWiggle(true)
                else
                    self:StartEggWiggle()
                end
            else
                self:StopEggWiggle()
                self.frame.PetTexture:SetAlpha(1)
                self.frame.PetTexture:SetTexture(ICON_EGG)
                self.frame.PetTexture:Show()
                if self.frame.EggModel then
                    self.frame.EggModel:Hide()
                end
            end
        else
            self:StopEggWiggle()
            local hasPetModel = self:EnsureAdolescentModel()
            if hasPetModel and self.frame.EggModel then
                self.frame.PetTexture:Hide()
                self.frame.EggModel:Show()
                self:ApplyModelControlState("adolescent")
            else
                if self.frame.EggModel then
                    self.frame.EggModel:Hide()
                end
                self.frame.PetTexture:Show()
                self.frame.PetTexture:SetAlpha(1)
                self.frame.PetTexture:SetTexture(ICON_MURLOC)
            end
        end
    else
        self:StopEggWiggle()
        if self.frame.EggModel then
            self.frame.EggModel:Hide()
        end
    end

    if self.frame.HabitatModelInput then
        self.frame.HabitatModelInput:SetShown(MODEL_CONTROLS_ENABLED and habitatActive and self.frame.EggModel and self.frame.EggModel:IsShown())
    end
    if self.frame.HabitatResetModelButton then
        self.frame.HabitatResetModelButton:SetShown(MODEL_CONTROLS_ENABLED and habitatActive and self.frame.EggModel and self.frame.EggModel:IsShown())
    end

    if pet.stage == "egg" then
        self.frame._petModelResolved = nil
        self.frame._petModelLoaded = nil
        self.frame._petModelSource = nil
    else
        self.frame._eggModelResolved = nil
        self.frame._eggModelLoaded = nil
        self.frame._eggModelSource = nil
    end

    if not self.db.profile.welcomeSeen then
        self.frame.WelcomePanel:Show()
        if self.RefreshWelcomeTutorial then
            self:RefreshWelcomeTutorial()
        end
    else
        self.frame.WelcomePanel:Hide()
    end

    if force and self.frame:IsShown() then
        self.frame:Raise()
    end
end

function MM:GetChallengeCardRewardText(challenge, eggStage)
    if not challenge then
        return ""
    end
    local function ColorText(text, r, g, b)
        local rr = math.floor((r or 1) * 255 + 0.5)
        local gg = math.floor((g or 1) * 255 + 0.5)
        local bb = math.floor((b or 1) * 255 + 0.5)
        return string.format("|cff%02x%02x%02x%s|r", rr, gg, bb, tostring(text or ""))
    end

    local WHITE_R, WHITE_G, WHITE_B = 1, 1, 1
    local HUNGER_R, HUNGER_G, HUNGER_B = 0.38, 0.92, 0.57
    local JOY_R, JOY_G, JOY_B = 0.35, 0.72, 1.0
    local ENERGY_R, ENERGY_G, ENERGY_B = 0.97, 0.8, 0.31
    local CLEAN_R, CLEAN_G, CLEAN_B = 0.76, 0.58, 0.95

    if eggStage then
        local hatchReward = math.floor(tonumber(challenge.rewardHatch) or 0)
        if hatchReward <= 0 then
            hatchReward = 2
        end
        return ColorText(string.format("+%d%%", hatchReward), COLOR_HATCH[1], COLOR_HATCH[2], COLOR_HATCH[3])
    end
    local parts = {}
    local shellReward = (self.GetEffectiveChallengeShellReward and self:GetEffectiveChallengeShellReward(challenge)) or (tonumber(challenge.reward) or 0)
    if shellReward > 0 then
        parts[#parts + 1] = ColorText(string.format("+%d shells", shellReward), COLOR_SHELL[1], COLOR_SHELL[2], COLOR_SHELL[3])
    end
    if challenge.rewardAllNeeds and challenge.rewardAllNeeds > 0 then
        parts[#parts + 1] = ColorText(string.format("+%d%% all", challenge.rewardAllNeeds), WHITE_R, WHITE_G, WHITE_B)
    elseif challenge.rewardNeeds then
        if (challenge.rewardNeeds.hunger or 0) > 0 then parts[#parts + 1] = ColorText(string.format("+%d%%", challenge.rewardNeeds.hunger), HUNGER_R, HUNGER_G, HUNGER_B) end
        if (challenge.rewardNeeds.joy or 0) > 0 then parts[#parts + 1] = ColorText(string.format("+%d%%", challenge.rewardNeeds.joy), JOY_R, JOY_G, JOY_B) end
        if (challenge.rewardNeeds.energy or 0) > 0 then parts[#parts + 1] = ColorText(string.format("+%d%%", challenge.rewardNeeds.energy), ENERGY_R, ENERGY_G, ENERGY_B) end
        if (challenge.rewardNeeds.cleanliness or 0) > 0 then parts[#parts + 1] = ColorText(string.format("+%d%%", challenge.rewardNeeds.cleanliness), CLEAN_R, CLEAN_G, CLEAN_B) end
    end
    return #parts > 0 and table.concat(parts, ", ") or "reward"
end

function MM:GetChallengeTooltipDescription(challenge, eggStage)
    if not challenge then
        return "No challenge is currently assigned to this card."
    end

    local t = tostring(challenge.type or "")
    local zone = tostring(challenge.zone or GetRealZoneText() or "your current zone")
    local petName = self:GetPetDisplayName()

    if eggStage then
        if t == "quest_turnin" then
            return "Turn in completed quests. Each turn-in helps settle and develop the egg."
        elseif t == "zone_activity" then
            if not challenge.zone or challenge.zone == "" then
                return "Use egg-care actions to build hatch momentum."
            end
            return string.format("Use egg-care actions while in %s to build hatch momentum.", zone)
        elseif t == "soothe" then
            return "Use Soothe Egg to calm your hatchling-in-progress and advance this task."
        elseif t == "warm" then
            return "Use Warm Egg to keep the shell cozy and improve hatching progress."
        elseif t == "clean" then
            return "Use Clean Egg to tend the shell and keep hatching conditions stable."
        end
    end

    if t == "visit_major_city" then
        return "Enter any major capital city area to complete this objective."
    elseif t == "visit_inn" then
        return "Enter an inn/rest area. Crossing into a rested zone gives credit."
    elseif t == "sleep_rested" then
        return "While in a rested area, use /sleep once."
    elseif t == "quest_turnin" then
        return "Turn in completed quests from your quest log."
    elseif t == "zone_activity" then
        return string.format("Perform caretaking actions while in %s.", zone)
    elseif t == "loot_any" then
        return string.format("Gather or loot in %s. Profession gathering and similar loot actions count.", zone)
    elseif t == "fish_zone" then
        return string.format("Catch a fish in %s to progress this challenge.", zone)
    elseif t == "eat_food" then
        return string.format("Eat food yourself so %s can share in the boost.", petName)
    elseif t == "feed" then
        return string.format("Use feed actions on %s.", petName)
    elseif t == "play" then
        return string.format("Use play actions with %s.", petName)
    elseif t == "rest" then
        return string.format("Use rest actions for %s.", petName)
    elseif t == "clean" then
        return "Use clean actions in the habitat."
    elseif t == "emote" then
        return "Use any emote command once (or as required)."
    elseif t == "emote_cheer" then
        return "Use /cheer once."
    elseif t == "emote_lean" then
        return "Use /lean once."
    elseif t == "emote_rude" then
        return "Use /rude once."
    elseif t == "emote_taunt" then
        return "Use /taunt once."
    elseif t == "emote_dance" then
        return "Use /dance once."
    elseif t == "emote_salute" then
        return "Use /salute once."
    elseif t == "emote_bow" then
        return "Use /bow once."
    elseif t == "emote_wave" then
        return "Use /wave once."
    elseif t == "emote_laugh" then
        return "Use /laugh once."
    elseif t == "emote_roar" then
        return "Use /roar once."
    elseif t == "emote_sleep" then
        return "Use /sleep once."
    elseif t == "emote_fart_rested" then
        return "While in a rested area, use /fart once."
    elseif t == "swim" then
        return "Enter water and start swimming."
    elseif t == "swim_short" then
        return "Stay swimming for 3 seconds total."
    elseif t == "swim_long" then
        return "Stay swimming for 10 seconds total."
    elseif t == "drink_any" then
        return "Use a drink item effect once."
    elseif t == "create_fire" then
        return "Create a campfire/fire effect (typically via cooking utility)."
    elseif t == "use_potion" then
        return "Use a potion/phial/elixir-type effect once."
    elseif t == "summon_companion" then
        return "Summon a mount or companion pet."
    elseif t == "dungeon_complete" then
        return "Finish a dungeon and receive completion credit."
    elseif t == "use_toy" then
        return "Use any toy from the Blizzard Collections Toy Box."
    elseif t == "work_ticks" then
        return "Keep your murloc scavenging; each paid scavenging tick advances this."
    elseif t == "earn_shells" then
        return "Earn shells from scavenging payouts."
    end

    return "Complete the listed objective to claim the reward and refresh this challenge."
end

function MM:ShowChallengeTooltip(index, owner)
    local economy = self:GetEconomy()
    local challenges = (economy and economy.challenges and economy.challenges.active) or {}
    local challenge = challenges[index]
    if not challenge then
        return
    end

    local pet = self:GetPet()
    local eggStage = pet and pet.stage == "egg"
    local state
    if self.GetChallengeProgressText then
        state = self:GetChallengeProgressText(challenge)
    else
        state = string.format("%d/%d", tonumber(challenge.progress or 0), tonumber(challenge.goal or 0))
    end
    local rewardText = self:GetChallengeCardRewardText(challenge, eggStage)
    local description = self:GetChallengeTooltipDescription(challenge, eggStage)

    GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
    GameTooltip:AddLine(challenge.label or "Challenge", 1.0, 0.82, 0.0, true)
    GameTooltip:AddLine(description, 0.92, 0.92, 0.92, true)
    GameTooltip:AddLine(" ")
    GameTooltip:AddLine(string.format("Progress: %s", state), 1.0, 1.0, 1.0)
    GameTooltip:AddLine(string.format("Reward: %s", rewardText), 1.0, 1.0, 1.0, true)
    GameTooltip:Show()
end

local CHALLENGE_CARD_GLOW_DURATION = 1.45
local CHALLENGE_CARD_GLOW_PULSES = 2

local function BuildChallengeCardText(owner, challenge, eggStage)
    if not challenge then
        return "No Challenge"
    end
    local state
    if owner.GetChallengeProgressText then
        state = owner:GetChallengeProgressText(challenge)
    else
        state = string.format("%d/%d", challenge.progress or 0, challenge.goal or 0)
    end
    return string.format(
        "%s\n(%s) [%s]",
        challenge.label or "Challenge",
        state,
        owner:GetChallengeCardRewardText(challenge, eggStage)
    )
end

function MM:BuildTrackerChallengeRowText(challenge, eggStage)
    if not challenge then
        return "No challenge in this slot."
    end
    local state = self.GetChallengeProgressText and self:GetChallengeProgressText(challenge)
    if not state then
        local progress = math.floor(tonumber(challenge.progress) or 0)
        local goal = math.floor(tonumber(challenge.goal) or 0)
        state = string.format("%d/%d", progress, goal)
    end
    local rewardText = self:GetChallengeCardRewardText(challenge, eggStage)
    return string.format("%s (%s) [%s]", challenge.label or "Challenge", state, rewardText)
end

function MM:BeginChallengeCardTransition(cellEntry, nextChallengeID, nextText)
    if not cellEntry or not cellEntry.text then
        return
    end
    cellEntry._transition = {
        phase = "glow",
        elapsed = 0,
    }
    cellEntry._activeChallengeID = nextChallengeID
    cellEntry._displayText = nextText or ""
    cellEntry.text:SetText(nextText or "")
    cellEntry.text:SetAlpha(1)
    if cellEntry.flash then
        cellEntry.flash:SetAlpha(0)
    end
    if cellEntry.glow then
        cellEntry.glow:SetAlpha(0)
    end
end

function MM:TickChallengeCard(index, elapsed)
    if not self.frame or not self.frame.ChallengeCells then
        return
    end

    local cellEntry = self.frame.ChallengeCells[index]
    if not cellEntry then
        return
    end

    local transition = cellEntry._transition
    if transition and cellEntry.text then
        transition.elapsed = (transition.elapsed or 0) + (elapsed or 0)
        local glow = cellEntry.glow

        if transition.phase == "glow" then
            local d = CHALLENGE_CARD_GLOW_DURATION
            local t = self:Clamp(transition.elapsed / d, 0, 1)
            local envelope = math.sin(t * math.pi)
            local pulse = math.sin(t * math.pi * CHALLENGE_CARD_GLOW_PULSES)
            local a = math.max(0, pulse) * envelope
            if glow then
                glow:SetAlpha(a)
            end
            if transition.elapsed >= d then
                if glow then
                    glow:SetAlpha(0)
                end
                cellEntry._transition = nil
            end
            return
        end
    end

    cellEntry._elapsed = (cellEntry._elapsed or 0) + (elapsed or 0)
    if cellEntry._elapsed < 0.1 then
        return
    end
    cellEntry._elapsed = 0

    self:RefreshChallengeCard(index)
end

function MM:BeginTrackerChallengeRowTransition(row, nextChallengeID, nextText)
    if not row or not row.text then
        return
    end
    row._transition = {
        phase = "glow",
        elapsed = 0,
    }
    row._activeChallengeID = nextChallengeID
    row._displayText = nextText or ""
    row.text:SetText(nextText or "")
    row.text:SetAlpha(1)
    if row.glow then
        row.glow:SetAlpha(0)
    end
end

function MM:TickTrackerChallengeRow(index, elapsed)
    if not self.frame or not self.frame.TrackerChallengeRows then
        return
    end

    local row = self.frame.TrackerChallengeRows[index]
    if not row or not row.text then
        return
    end

    local transition = row._transition
    if not transition then
        return
    end

    transition.elapsed = (transition.elapsed or 0) + (elapsed or 0)
    local glow = row.glow

    if transition.phase == "glow" then
        local d = CHALLENGE_CARD_GLOW_DURATION
        local t = self:Clamp(transition.elapsed / d, 0, 1)
        local envelope = math.sin(t * math.pi)
        local pulse = math.sin(t * math.pi * CHALLENGE_CARD_GLOW_PULSES)
        local a = math.max(0, pulse) * envelope
        if glow then
            glow:SetAlpha(a)
        end
        if transition.elapsed >= d then
            if glow then
                glow:SetAlpha(0)
            end
            row._transition = nil
        end
        return
    end
end

function MM:RefreshChallengeCard(index)
    if not self.frame or not self.frame.ChallengeCells then
        return
    end

    local cellEntry = self.frame.ChallengeCells[index]
    local cellFrame = cellEntry and cellEntry.frame
    local cellText = cellEntry and cellEntry.text
    local reloadButton = cellEntry and cellEntry.reloadButton
    local reloadCooldownText = cellEntry and cellEntry.reloadCooldownText
    if not cellText then
        return
    end

    local pet = self:GetPet()
    local eggStage = pet and pet.stage == "egg"
    local store = self:GetStore()
    local economy = self:GetEconomy() or {}
    local challenges = (economy.challenges and economy.challenges.active) or {}
    local challenge = challenges[index]
    local nextChallengeID = challenge and tostring(challenge.id or "") or ""
    local nextText = BuildChallengeCardText(self, challenge, eggStage)

    if challenge then
        if reloadButton then
            local reloadCost = 20
            local remaining = (self.GetChallengeReloadRemaining and self:GetChallengeReloadRemaining(index)) or 0
            local eggRerollRemaining = (self.GetChallengeRerollRemaining and self:GetChallengeRerollRemaining()) or 0
            local canAfford = (store.shells or 0) >= reloadCost
            local cooldownRemaining = eggStage and eggRerollRemaining or remaining
            if eggStage then
                reloadButton:SetEnabled(eggRerollRemaining <= 0)
            elseif remaining > 0 then
                reloadButton:SetEnabled(false)
            else
                reloadButton:SetEnabled(canAfford)
            end
            reloadButton:Show()
            if reloadCooldownText then
                if cooldownRemaining > 0 then
                    local displaySeconds = math.max(1, math.ceil(cooldownRemaining))
                    local mins = math.floor(displaySeconds / 60)
                    local secs = displaySeconds % 60
                    reloadCooldownText:SetText(string.format("CD %d:%02d", mins, secs))
                    reloadCooldownText:SetTextColor(1.0, 0.32, 0.32)
                    reloadCooldownText:Show()
                else
                    reloadCooldownText:Hide()
                end
            end
        end

        local effectiveReward = (self.GetEffectiveChallengeShellReward and self:GetEffectiveChallengeShellReward(challenge)) or (tonumber(challenge.reward) or 0)
        local renderKey = string.format(
            "%s|%d|%d|%d|%d|%d",
            tostring(challenge.id or ""),
            tonumber(challenge.progress or 0),
            tonumber(challenge.goal or 0),
            tonumber(effectiveReward or 0),
            tonumber(challenge.rewardAllNeeds or 0),
            eggStage and 1 or 0
        )
        if cellEntry._lastRenderKey == renderKey then
            return
        end
        cellEntry._lastRenderKey = renderKey

        if cellEntry._activeChallengeID and cellEntry._activeChallengeID ~= nextChallengeID then
            self:BeginChallengeCardTransition(cellEntry, nextChallengeID, nextText)
            return
        end

        cellEntry._activeChallengeID = nextChallengeID
        cellEntry._displayText = nextText
        cellText:SetText(nextText)
        cellText:SetAlpha(1)
        if cellEntry.flash then
            cellEntry.flash:SetAlpha(0)
        end
        if cellEntry.glow then
            cellEntry.glow:SetAlpha(0)
        end
        if cellFrame then
            cellFrame:SetAlpha(1)
        end
        return
    end

    if reloadButton then
        reloadButton:Hide()
    end
    if reloadCooldownText then
        reloadCooldownText:Hide()
    end
    if cellEntry._lastRenderKey == "__empty__" then
        return
    end
    cellEntry._lastRenderKey = "__empty__"
    cellEntry._activeChallengeID = nil
    cellEntry._displayText = nextText
    cellEntry._transition = nil
    cellText:SetText(nextText)
    cellText:SetAlpha(1)
    if cellEntry.flash then
        cellEntry.flash:SetAlpha(0)
    end
    if cellEntry.glow then
        cellEntry.glow:SetAlpha(0)
    end
    if cellFrame then
        cellFrame:SetAlpha(1)
    end
end
