--------------------------------------------------------------------------------
-- S-Frames: Bank Module GUI (Bags/Bank.lua)
-- Unified custom interface for the player's Bank and extended Bank Bags
--------------------------------------------------------------------------------

SFrames.Bags.Bank = {}
local SFBankFrame = nil
local ItemSlots = {}
local isClosing = false -- Guard to prevent close闂傚倸鍊烽悞锕傚礈濮樿泛纾婚柛娑卞枟閸欏繘鏌嶈閹叉矠nt闂傚倸鍊烽悞锕傚礈濮樿泛纾婚柛娑卞枙缁诲棛绱掑顔界厪se recursion

local SLOT_SIZE = 34
local SPACING   = 6
local MARGIN    = 10
local TOP_OFFSET = 52  -- Space for title + search bar row
local BOTTOM_OFFSET = 34 -- Space for bank bag slot controls
local TEXT_EMPTY = "\231\169\186"
local TEXT_ITEM = "\231\137\169\229\147\129"
local TEXT_BANK_TITLE = "\233\147\182\232\161\140"
local TEXT_SORT = "\230\149\180\231\144\134"
local TEXT_BUY_SLOT = "\232\180\173\228\185\176\230\160\143\228\189\141"
local TEXT_UNAVAILABLE_OFFLINE = "\231\166\187\231\186\191\230\168\161\229\188\143\228\184\139\228\184\141\229\143\175\231\148\168"
local TEXT_BANK_BAG_SLOT = "\233\147\182\232\161\140\232\131\140\229\140\133\230\167\189"
local TEXT_BANK_BAG = "\233\147\182\232\161\140\232\131\140\229\140\133"
local TEXT_LOCKED_BANK_SLOT = "\229\183\178\233\148\129\229\174\154\231\154\132\233\147\182\232\161\140\232\131\140\229\140\133\230\167\189"
local TEXT_CLICK_BUY = "\231\130\185\229\135\187\232\180\173\228\185\176\232\175\165\230\160\143\228\189\141"
local TEXT_BUY_PREV_FIRST = "\232\175\183\229\133\136\232\180\173\228\185\176\229\137\141\228\184\128\228\184\170\230\160\143\228\189\141"
local TEXT_DRAG_EQUIP = "\230\139\150\229\133\165\232\131\140\229\140\133\229\143\175\232\163\133\229\164\135\229\136\176\230\173\164\230\160\143\228\189\141"
local TEXT_RIGHT_PICKUP = "\229\143\179\233\148\174\229\143\150\228\184\139\229\183\178\232\163\133\229\164\135\232\131\140\229\140\133"
local TEXT_CHARACTER = "\232\167\146\232\137\178"
local TEXT_ONLINE = "\229\156\168\231\186\191"
local TEXT_OFFLINE = "\231\166\187\231\186\191"
local TEXT_LAYOUT_ERR = "\233\147\182\232\161\140\229\184\131\229\177\128\230\155\180\230\150\176\229\164\177\232\180\165\239\188\154"
local TEXT_SLOT_UNIT = "\230\160\188"
local DEFAULT_BAG_ICON = "Interface\\Buttons\\Button-Backpack-Up"
local EMPTY_BAG_ICON = "Interface\\Icons\\INV_Misc_Bag_08"
local CHARACTER_SELECTOR_ICON = "Interface\\CHARACTERFRAME\\TemporaryPortrait-Female-Human"
local PANEL_BG_ALPHA = 0.55
local SLOT_BG_ALPHA = 0.22

local _A = SFrames.ActiveTheme

local bankSearchText = ""  -- Current search filter text
local BankBagButtons = {}
local bankLayoutErrorStamp = nil

local BANK_BAG_SIZE = 22
local BANK_BAG_SPACING = 3
local BANK_BAG_COUNT = 6
local BANK_BAG_FIRST_ID = 5
local BANK_BAG_LAST_ID = BANK_BAG_FIRST_ID + BANK_BAG_COUNT - 1
local bankBagInvSlotCache = {}

local function SafeBankUpdateLayout()
    local ok, err = pcall(function() SFrames.Bags.Bank:UpdateLayout() end)
    if not ok and err then
        if bankLayoutErrorStamp ~= err then
            bankLayoutErrorStamp = err
            if SFrames and SFrames.Print then
                SFrames:Print("闂備胶鍋撻崕濂搞€侀幋锔藉仼鐎光偓閸曨剚銆冮梺鍛婂浮閺€閬嶅蓟婵犲洦鐓ユ繛鍡樺俯閸? " .. tostring(err))
            end
        end
    end
end

-- Override with clean localized error text.
SafeBankUpdateLayout = function()
    local ok, err = pcall(function() SFrames.Bags.Bank:UpdateLayout() end)
    if not ok and err then
        if bankLayoutErrorStamp ~= err then
            bankLayoutErrorStamp = err
            if SFrames and SFrames.Print then
                SFrames:Print(TEXT_LAYOUT_ERR .. tostring(err))
            end
        end
    end
end

local function SaveBankFramePosition()
    if not (SFBankFrame and SFramesDB and SFramesDB.Bags) then return end
    local point, _, relPoint, x, y = SFBankFrame:GetPoint()
    if not point or not relPoint then return end
    SFramesDB.Bags.bankPosition = {
        point = point,
        relPoint = relPoint,
        x = x or 0,
        y = y or 0,
    }
end

local function ApplyBankFramePosition()
    if not SFBankFrame then return end
    SFBankFrame:ClearAllPoints()

    local pos = SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bankPosition
    if pos and pos.point and pos.relPoint and type(pos.x) == "number" and type(pos.y) == "number" then
        SFBankFrame:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y)
    else
        -- Default to right side; bag frame defaults to left side.
        SFBankFrame:SetPoint("CENTER", UIParent, "CENTER", 360, 0)
    end
end

-- Bank sort: sorts main bank and bank bag containers
local function SortBank()
    -- WoW 1.12 doesn't have SortBags() but we can do a basic consolidation
    -- by using the container sort API if available
    if SortBankBags then
        SortBankBags()
    end
    if SortBags then
        -- also sort player bags since items may move there
        SortBags()
    end
end

local function SetTooltipFromBankContainerItem(bagID, slotID)
    if not GameTooltip then return false end
    GameTooltip:ClearLines()

    if GameTooltip.SetBagItem then
        local ok = pcall(function() GameTooltip:SetBagItem(bagID, slotID) end)
        if ok then
            local left1 = _G["GameTooltipTextLeft1"]
            if left1 and left1:GetText() and left1:GetText() ~= "" then
                return true
            end
        end
    end

    local link = GetContainerItemLink(bagID, slotID)
    if link then
        local ok = pcall(function() GameTooltip:SetHyperlink(link) end)
        if ok then
            local left1 = _G["GameTooltipTextLeft1"]
            if left1 and left1:GetText() and left1:GetText() ~= "" then
                return true
            end
        end

        local name = GetItemInfo(link)
        if name and name ~= "" then
            GameTooltip:SetText(name, 1, 1, 1)
            return true
        end
    end

    return false
end

local function HasTooltipText()
    local left1 = _G["GameTooltipTextLeft1"]
    return left1 and left1:GetText() and left1:GetText() ~= ""
end

local function GetItemNameFromLink(link)
    if type(link) ~= "string" or link == "" then
        return nil
    end

    local name = GetItemInfo(link)
    if name and name ~= "" then
        return name
    end

    local _, _, parsed = string.find(link, "%[(.+)%]")
    if parsed and parsed ~= "" then
        return parsed
    end

    return nil
end

local function IsAsciiText(text)
    if type(text) ~= "string" then return false end
    return string.find(text, "[\128-\255]") == nil
end

local function TextMatchesSearch(name, query)
    if not query or query == "" then return true end
    if not name or name == "" then return false end

    if string.find(name, query, 1, true) then
        return true
    end

    -- Keep Chinese/GBK safe: only do lowercase matching for pure ASCII strings.
    if IsAsciiText(name) and IsAsciiText(query) then
        return string.find(string.lower(name), string.lower(query), 1, true) ~= nil
    end

    return false
end

local function GetSafeCoinText(copper)
    local value = tonumber(copper) or 0
    if value <= 0 then
        return "0c"
    end

    if GetCoinTextureString then
        local ok, text = pcall(function() return GetCoinTextureString(value) end)
        if ok and text and text ~= "" then
            return text
        end
    end

    local g = math.floor(value / 10000)
    local s = math.floor(math.mod(value, 10000) / 100)
    local c = math.mod(value, 100)
    return string.format("%dg %ds %dc", g, s, c)
end

local function CreateCoinDisplay(parent, frameName)
    local frame = CreateFrame("Frame", frameName, parent, "SmallMoneyFrameTemplate")
    frame:SetFrameStrata(parent:GetFrameStrata())
    frame:SetFrameLevel(parent:GetFrameLevel() + 30)
    frame:SetWidth(140)
    frame:SetHeight(16)
    return frame
end

local function SetCoinDisplayMoney(display, copper)
    if not display then return end
    local value = tonumber(copper) or 0
    if value < 0 then value = 0 end

    if SmallMoneyFrame_SetAmount then
        local ok = pcall(function() SmallMoneyFrame_SetAmount(display, value) end)
        if ok then return end
    end

    if MoneyFrame_Update and display.GetName then
        local frameName = display:GetName()
        if frameName and frameName ~= "" then
            pcall(function() MoneyFrame_Update(frameName, value) end)
        end
    end
end

local function IsValidBankInvSlot(id)
    return type(id) == "number" and id >= 39 and id <= 74
end

local function GetMainBankInvSlotID(slotID)
    if not slotID or slotID <= 0 then return nil end

    local liveBtn = _G["BankFrameItem" .. slotID]
    if liveBtn and liveBtn.GetID then
        local id = liveBtn:GetID()
        if IsValidBankInvSlot(id) then
            return id
        end
    end

    if not BankButtonIDToInvSlotID then return nil end

    local ok, invSlot = pcall(function() return BankButtonIDToInvSlotID(slotID) end)
    if ok and IsValidBankInvSlot(invSlot) then
        return invSlot
    end

    ok, invSlot = pcall(function() return BankButtonIDToInvSlotID(slotID, 0) end)
    if ok and IsValidBankInvSlot(invSlot) then
        return invSlot
    end

    ok, invSlot = pcall(function() return BankButtonIDToInvSlotID(slotID, 1) end)
    if ok and IsValidBankInvSlot(invSlot) then
        return invSlot
    end

    return nil
end

-- Override tooltip resolution with a stronger fallback chain for main bank slots.
SetTooltipFromBankContainerItem = function(bagID, slotID, cachedLink, cachedName)
    if not GameTooltip then return false end
    GameTooltip:ClearLines()

    if bagID == -1 and GameTooltip.SetInventoryItem then
        local invSlot = GetMainBankInvSlotID(slotID)
        if invSlot then
            local ok = pcall(function() GameTooltip:SetInventoryItem("player", invSlot) end)
            if ok and HasTooltipText() then
                return true
            end
        end
    end

    if GameTooltip.SetBagItem then
        local ok = pcall(function() GameTooltip:SetBagItem(bagID, slotID) end)
        if ok and HasTooltipText() then
            return true
        end
    end

    local link = GetContainerItemLink(bagID, slotID) or cachedLink
    if link then
        local ok = pcall(function() GameTooltip:SetHyperlink(link) end)
        if ok and HasTooltipText() then
            return true
        end

        local name = GetItemInfo(link)
        if name and name ~= "" then
            GameTooltip:SetText(name, 1, 1, 1)
            return true
        end
    end

    if cachedName and cachedName ~= "" then
        GameTooltip:SetText(cachedName, 1, 1, 1)
        return true
    end

    return false
end

local function SafeGetInventoryItemLink(unit, invSlot)
    if not invSlot then return nil end
    local ok, link = pcall(function() return GetInventoryItemLink(unit, invSlot) end)
    if ok then return link end
    return nil
end

local function SafeGetInventoryItemTexture(unit, invSlot)
    if not invSlot then return nil end
    local ok, tex = pcall(function() return GetInventoryItemTexture(unit, invSlot) end)
    if ok then return tex end
    return nil
end

local function SafeGetInventorySlotByName(slotName)
    if not GetInventorySlotInfo then return nil end
    local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end)
    if ok and type(slotID) == "number" and slotID > 0 then
        return slotID
    end
    return nil
end

local function IsPaperdollBagPlaceholder(tex)
    if type(tex) ~= "string" then return false end
    local lower = string.lower(tex)
    lower = string.gsub(lower, "\\", "/")

    if string.find(lower, "ui-paperdoll-slot-bag", 1, true) then
        return true
    end

    if string.find(lower, "paperdoll", 1, true) and
       string.find(lower, "slot", 1, true) and
       string.find(lower, "bag", 1, true) then
        return true
    end

    return false
end

local function IsDisabledIconTexture(tex)
    if type(tex) ~= "string" then return false end
    local lower = string.lower(tex)
    lower = string.gsub(lower, "\\", "/")
    return string.find(lower, "disabled", 1, true) ~= nil
end

local function IsDefaultBankBagPlaceholder(tex)
    if type(tex) ~= "string" then return false end
    local lower = string.lower(tex)
    lower = string.gsub(lower, "\\", "/")
    if string.find(lower, "button-backpack-up", 1, true) then
        return true
    end
    if string.find(lower, "button-backpack-disabled", 1, true) then
        return true
    end
    return false
end

local function IsUsableBankBagIconTexture(tex)
    if type(tex) ~= "string" or tex == "" then
        return false
    end
    if IsPaperdollBagPlaceholder(tex) then
        return false
    end
    if IsDisabledIconTexture(tex) then
        return false
    end
    if IsDefaultBankBagPlaceholder(tex) then
        return false
    end
    return true
end

local function GetIconFromItemLink(link)
    if not link then return nil end

    local _, _, _, _, _, _, _, _, tex = GetItemInfo(link)
    if tex then return tex end

    local _, _, itemID = string.find(link, "item:(%d+)")
    if itemID then
        local _, _, _, _, _, _, _, _, tex2 = GetItemInfo("item:" .. itemID)
        if tex2 then return tex2 end
    end

    return nil
end

local function IsBagItemLink(link)
    if type(link) ~= "string" or link == "" then
        return false
    end

    local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link)
    return equipLoc == "INVTYPE_BAG"
end

local function GetBagLinkState(link)
    if type(link) ~= "string" or link == "" then
        return "invalid"
    end

    local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link)
    if equipLoc == "INVTYPE_BAG" then
        return "bag"
    end
    if equipLoc == nil or equipLoc == "" then
        return "unknown"
    end
    return "invalid"
end

local function AddInvSlotCandidate(candidates, seen, slotID)
    if type(slotID) ~= "number" or slotID <= 0 then
        return
    end
    if seen[slotID] then
        return
    end
    seen[slotID] = true
    table.insert(candidates, slotID)
end

local function GetLiveBankBagButton(index)
    local names = {
        "BankFrameBag" .. index,
        "BankFrameBag" .. index .. "Slot",
    }

    for _, name in ipairs(names) do
        local btn = _G[name]
        if btn and btn.GetObjectType and btn:GetObjectType() == "Button" then
            return btn
        end
    end

    return nil
end

local function AddBankInvSlotCandidate(candidates, seen, slotID)
    if type(slotID) ~= "number" or slotID <= 0 then
        return
    end

    -- Real bank bag equipment slots are not normal character inventory slots.
    if slotID <= 23 then
        return
    end

    AddInvSlotCandidate(candidates, seen, slotID)
end

local function IsBankBagInvSlotID(slotID)
    return type(slotID) == "number" and slotID > 23
end

local function GetBankBagInvSlotID(index)
    if type(index) ~= "number" or index <= 0 then
        return nil
    end

    local cached = bankBagInvSlotCache[index]
    if type(cached) == "number" and cached > 23 then
        return cached
    end
    bankBagInvSlotCache[index] = nil

    local bagID = index + (BANK_BAG_FIRST_ID - 1)
    local function TryBankButtonID(buttonID, isBank)
        if not BankButtonIDToInvSlotID then return nil end
        local ok, slotID = pcall(function() return BankButtonIDToInvSlotID(buttonID, isBank) end)
        if ok and IsBankBagInvSlotID(slotID) then
            return slotID
        end
        return nil
    end

    local function AcceptIfBankSlot(slotID)
        if IsBankBagInvSlotID(slotID) then
            return slotID
        end
        return nil
    end

    -- Primary mapping: bag container id + isBank=1.
    local slot = TryBankButtonID(bagID, 1)
    if slot then
        bankBagInvSlotCache[index] = slot
        return slot
    end

    -- Compatibility fallback: some clients may expect 1..N as first arg.
    slot = TryBankButtonID(index, 1)
    if slot then
        bankBagInvSlotCache[index] = slot
        return slot
    end

    -- Only accept legacy guesses when they actually hold a bag item.
    local function AcceptIfBag(slotID)
        slotID = AcceptIfBankSlot(slotID)
        if not slotID then return nil end
        local link = SafeGetInventoryItemLink("player", slotID)
        if link and IsBagItemLink(link) then
            return slotID
        end
        return nil
    end

    slot = TryBankButtonID(bagID, nil)
    slot = AcceptIfBag(slot)
    if slot then
        bankBagInvSlotCache[index] = slot
        return slot
    end

    slot = TryBankButtonID(index, nil)
    slot = AcceptIfBag(slot)
    if slot then
        bankBagInvSlotCache[index] = slot
        return slot
    end

    if ContainerIDToInventoryID then
        local ok, result = pcall(function() return ContainerIDToInventoryID(bagID) end)
        if ok then
            slot = AcceptIfBankSlot(result)
            if slot then
                bankBagInvSlotCache[index] = slot
                return slot
            end
        end
    end

    -- BankBagSlotN is a stable bank bag equipment slot token and can be empty.
    slot = AcceptIfBankSlot(SafeGetInventorySlotByName("BankBagSlot" .. index))
    if slot then
        bankBagInvSlotCache[index] = slot
        return slot
    end

    -- Alternate token fallback used by a few legacy clients.
    slot = AcceptIfBankSlot(SafeGetInventorySlotByName("BankBag" .. index))
    if slot then
        bankBagInvSlotCache[index] = slot
        return slot
    end

    slot = AcceptIfBag(SafeGetInventorySlotByName("BankSlot" .. index))
    if slot then
        bankBagInvSlotCache[index] = slot
        return slot
    end

    local liveBtn = GetLiveBankBagButton(index)
    if liveBtn and liveBtn.GetID then
        local btnID = liveBtn:GetID()
        slot = TryBankButtonID(btnID, 1) or AcceptIfBankSlot(btnID) or AcceptIfBag(btnID)
        if slot then
            bankBagInvSlotCache[index] = slot
            return slot
        end
    end

    return nil
end

local function GetLiveBankBagIconTexture(index)
    local btn = GetLiveBankBagButton(index)
    if btn then
        local icon = _G[btn:GetName() .. "IconTexture"] or _G[btn:GetName() .. "Icon"]
        if icon and icon.GetTexture then
            local tex = icon:GetTexture()
            if type(tex) == "string" and tex ~= "" then
                return tex
            end
        end
    end

    local directIcon = _G["BankFrameBag" .. index .. "IconTexture"] or _G["BankFrameBag" .. index .. "Icon"]
    if directIcon and directIcon.GetTexture then
        local tex = directIcon:GetTexture()
        if type(tex) == "string" and tex ~= "" then
            return tex
        end
    end

    return nil
end

local function IsBankBagSlotUnlocked(index)
    local purchased = 0
    if GetNumBankSlots then
        purchased = GetNumBankSlots() or 0
    end
    return index <= purchased
end

local function TryPurchaseBankSlot(index)
    if not PurchaseSlot then return false end
    if IsBankBagSlotUnlocked(index) then return false end
    local purchased = (GetNumBankSlots and GetNumBankSlots()) or 0
    local nextSlot = purchased + 1
    if index ~= nextSlot then return false end
    local ok = pcall(function() PurchaseSlot() end)
    return ok
end

local function PlaceCursorItemInBankBagSlot(index)
    if not CursorHasItem() then return false end
    local invSlot = GetBankBagInvSlotID(index)
    if not invSlot then return false end

    if EquipCursorItem then
        local equipOK = pcall(function() EquipCursorItem(invSlot) end)
        if equipOK and (not CursorHasItem()) then
            return true
        end
    end

    -- This works for both placing and swapping bags in vanilla.
    local ok = pcall(function() PickupBagFromSlot(invSlot) end)
    if ok and (not CursorHasItem()) then
        return true
    end

    -- Fallback path for clients where PickupBagFromSlot doesn't place cursor item.
    if PutItemInBag then
        pcall(function() PutItemInBag(invSlot) end)
    end
    if CursorHasItem() then
        pcall(function() PickupInventoryItem(invSlot) end)
    end
    return not CursorHasItem()
end

local function GetOfflineBankSlotState(offlineDB, slotIndex)
    local purchased = 0
    if offlineDB and type(offlineDB.bankSlots) == "number" then
        purchased = math.max(0, math.floor(offlineDB.bankSlots))
    end

    if purchased <= 0 and offlineDB and offlineDB.bankBags then
        for i = 1, BANK_BAG_COUNT do
            local meta = offlineDB.bankBags[i]
            local metaSize = 0
            if meta and type(meta.size) == "number" then
                metaSize = math.max(0, math.floor(meta.size))
            end
            if meta and (meta.unlocked or metaSize > 0) then
                purchased = math.max(purchased, i)
            end
        end
    end

    if purchased <= 0 and offlineDB and offlineDB.bank then
        for i = 1, BANK_BAG_COUNT do
            local bagData = offlineDB.bank[i + (BANK_BAG_FIRST_ID - 1)]
            local size = 0
            if bagData and type(bagData.size) == "number" then
                size = math.max(0, math.floor(bagData.size))
            end
            if size > 0 then
                purchased = math.max(purchased, i)
            end
        end
    end

    local bagID = slotIndex + (BANK_BAG_FIRST_ID - 1)
    local bagData = offlineDB and offlineDB.bank and offlineDB.bank[bagID]
    local bagSlots = 0
    if bagData and type(bagData.size) == "number" then
        bagSlots = math.max(0, math.floor(bagData.size))
    end

    local bagMeta = offlineDB and offlineDB.bankBags and offlineDB.bankBags[slotIndex]
    if bagMeta and type(bagMeta.size) == "number" and bagMeta.size > bagSlots then
        bagSlots = math.max(0, math.floor(bagMeta.size))
    end

    -- Migration fallback for old offline snapshots that did not store bankSlots:
    -- any slot with positive container size implies this slot is available.
    if bagSlots > 0 and slotIndex > purchased then
        purchased = slotIndex
    end

    local unlocked = (slotIndex <= purchased)
    if (not unlocked) and bagSlots > 0 then
        unlocked = true
    end
    if (not unlocked) and bagMeta and bagMeta.unlocked then
        unlocked = true
    end

    local link = nil
    local tex = nil
    local rawLink = bagMeta and bagMeta.link or nil
    if bagSlots > 0 then
        local linkState = GetBagLinkState(rawLink)
        if linkState == "bag" or linkState == "unknown" then
            link = rawLink
            tex = bagMeta and bagMeta.texture or nil
        end
    end

    return unlocked, bagSlots, link, tex, purchased
end

local function CreateBankBagButton(parent, index)
    local btn = CreateFrame("Button", "SFramesBankBagBtn" .. index, parent)
    btn:SetWidth(BANK_BAG_SIZE)
    btn:SetHeight(BANK_BAG_SIZE)
    btn.slotIndex = index
    btn:SetFrameStrata(parent:GetFrameStrata())
    btn:SetFrameLevel(parent:GetFrameLevel() + 20)

    -- Rounded backdrop (matching bag slot style)
    btn:SetBackdrop({
        bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 12,
        insets = { left = 2, right = 2, top = 2, bottom = 2 }
    })
    btn:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.9)
    btn:SetBackdropBorderColor(_A.slotBorder[1], _A.slotBorder[2], _A.slotBorder[3], _A.slotBorder[4] or 0.8)

    local icon = btn:CreateTexture(nil, "OVERLAY")
    icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2)
    icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2)
    icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    icon:SetBlendMode("BLEND")
    icon:SetVertexColor(1, 1, 1, 1)
    icon:SetAlpha(1)
    btn.icon = icon

    local lock = btn:CreateTexture(nil, "OVERLAY")
    lock:SetTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Up")
    lock:SetWidth(12)
    lock:SetHeight(12)
    lock:SetPoint("CENTER", btn, "CENTER", 0, 0)
    lock:Hide()
    btn.lockIcon = lock

    local highlight = btn:CreateTexture(nil, "HIGHLIGHT")
    highlight:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
    highlight:SetBlendMode("ADD")
    highlight:SetAllPoints(btn)

    btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    btn:RegisterForDrag("LeftButton")

    btn:SetScript("OnEnter", function()
        GameTooltip:SetOwner(this, "ANCHOR_TOP")
        GameTooltip:ClearLines()
        if SFrames.Bags.Bank.isOffline then
            local data = nil
            if SFrames.Bags.Bank.offlineChar and SFrames.Bags.Offline and SFrames.Bags.Offline.GetCharacterData then
                data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Bank.offlineChar)
            end

            local unlocked, bagSlots, bagLink = GetOfflineBankSlotState(data, this.slotIndex)
            if unlocked then
                local shown = false
                if bagSlots > 0 and bagLink then
                    local _, _, itemStr = string.find(bagLink, "(item:[%-?%d:]+)")
                    local ok = false
                    if itemStr then
                        ok = pcall(function() GameTooltip:SetHyperlink(itemStr) end)
                    else
                        ok = pcall(function() GameTooltip:SetHyperlink(bagLink) end)
                    end
                    shown = ok and HasTooltipText()

                    if not shown then
                        local name = GetItemNameFromLink(bagLink)
                        if name and name ~= "" then
                            GameTooltip:SetText(name, 1, 1, 1)
                            shown = true
                        end
                    end
                end

                if not shown then
                    if bagSlots > 0 then
                        GameTooltip:SetText(string.format("%s %d (%d%s)", TEXT_BANK_BAG, this.slotIndex, bagSlots, TEXT_SLOT_UNIT), 1, 1, 1)
                    else
                        GameTooltip:SetText(string.format("%s %d (%s)", TEXT_BANK_BAG_SLOT, this.slotIndex, TEXT_EMPTY), 0.9, 0.9, 0.9)
                    end
                end
                GameTooltip:AddLine(TEXT_OFFLINE, 0.75, 0.75, 0.75)
            else
                GameTooltip:SetText(TEXT_LOCKED_BANK_SLOT, 1, 0.2, 0.2)
                GameTooltip:AddLine(TEXT_OFFLINE, 0.75, 0.75, 0.75)
            end
        else
            local unlocked = this.unlocked
            if unlocked then
                local invSlot = GetBankBagInvSlotID(this.slotIndex)
                local bagSlots = GetContainerNumSlots(this.slotIndex + (BANK_BAG_FIRST_ID - 1)) or 0
                local shown = false

                if bagSlots > 0 and invSlot and GameTooltip.SetInventoryItem then
                    local ok = pcall(function() GameTooltip:SetInventoryItem("player", invSlot) end)
                    if ok and HasTooltipText() then
                        shown = true
                    end
                end

                if not shown then
                    local link = SafeGetInventoryItemLink("player", invSlot)
                    if bagSlots > 0 and link then
                        local ok = pcall(function() GameTooltip:SetHyperlink(link) end)
                        if ok and HasTooltipText() then
                            shown = true
                        end
                    end
                end

                if not shown then
                    if bagSlots > 0 then
                        GameTooltip:SetText(string.format("%s %d (%d%s)", TEXT_BANK_BAG, this.slotIndex, bagSlots, TEXT_SLOT_UNIT), 1, 1, 1)
                    else
                        GameTooltip:SetText(string.format("%s %d (%s)", TEXT_BANK_BAG_SLOT, this.slotIndex, TEXT_EMPTY), 0.9, 0.9, 0.9)
                    end
                end
                GameTooltip:AddLine(TEXT_DRAG_EQUIP, 0.7, 0.7, 0.7)
                GameTooltip:AddLine(TEXT_RIGHT_PICKUP, 0.7, 0.7, 0.7)
            else
                GameTooltip:SetText(TEXT_LOCKED_BANK_SLOT, 1, 0.2, 0.2)
                local purchased = (GetNumBankSlots and GetNumBankSlots()) or 0
                local nextSlot = purchased + 1
                if this.slotIndex == nextSlot then
                    local cost = (GetBankSlotCost and GetBankSlotCost()) or 0
                    if cost > 0 then
                        GameTooltip:AddLine(TEXT_CLICK_BUY, 1, 0.82, 0)
                        GameTooltip:AddLine(GetSafeCoinText(cost), 1, 0.82, 0)
                    else
                        GameTooltip:AddLine(TEXT_CLICK_BUY, 1, 0.82, 0)
                    end
                else
                    GameTooltip:AddLine(TEXT_BUY_PREV_FIRST, 0.7, 0.7, 0.7)
                end
            end
        end
        if this.unlocked and SFBankFrame and SFBankFrame:IsVisible() then
            if SFrames.Bags.Bank.isOffline then
                local data = nil
                if SFrames.Bags.Bank.offlineChar and SFrames.Bags.Offline and SFrames.Bags.Offline.GetCharacterData then
                    data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Bank.offlineChar)
                end
                local _, offlineBagSlots = GetOfflineBankSlotState(data, this.slotIndex)
                if offlineBagSlots > 0 then
                    SFrames.Bags.Bank:PreviewBankBagSlots(this.slotIndex + (BANK_BAG_FIRST_ID - 1))
                end
            else
                local bagSlots = GetContainerNumSlots(this.slotIndex + (BANK_BAG_FIRST_ID - 1)) or 0
                if bagSlots > 0 then
                    SFrames.Bags.Bank:PreviewBankBagSlots(this.slotIndex + (BANK_BAG_FIRST_ID - 1))
                end
            end
        end
        GameTooltip:Show()
    end)
    btn:SetScript("OnLeave", function()
        GameTooltip:Hide()
        if SFBankFrame and SFBankFrame:IsVisible() then
            SFrames.Bags.Bank:ClearBankBagPreview()
        end
    end)

    btn:SetScript("OnDragStart", function()
        if SFrames.Bags.Bank.isOffline then return end
        if not this.unlocked then return end
        local invSlot = GetBankBagInvSlotID(this.slotIndex)
        if invSlot then
            pcall(function() PickupBagFromSlot(invSlot) end)
        end
    end)

    btn:SetScript("OnReceiveDrag", function()
        if SFrames.Bags.Bank.isOffline then return end
        if not CursorHasItem() then return end

        if this.unlocked then
            PlaceCursorItemInBankBagSlot(this.slotIndex)
        else
            if TryPurchaseBankSlot(this.slotIndex) then
                if IsBankBagSlotUnlocked(this.slotIndex) and CursorHasItem() then
                    PlaceCursorItemInBankBagSlot(this.slotIndex)
                end
            end
        end

        SafeBankUpdateLayout()
    end)

    btn:SetScript("OnClick", function()
        if SFrames.Bags.Bank.isOffline then return end

        if CursorHasItem() then
            if this.unlocked then
                PlaceCursorItemInBankBagSlot(this.slotIndex)
            else
                if TryPurchaseBankSlot(this.slotIndex) and IsBankBagSlotUnlocked(this.slotIndex) then
                    PlaceCursorItemInBankBagSlot(this.slotIndex)
                end
            end
            SafeBankUpdateLayout()
            return
        end

        if this.unlocked then
            local invSlot = GetBankBagInvSlotID(this.slotIndex)
            if invSlot then
                pcall(function() PickupBagFromSlot(invSlot) end)
            end
        else
            TryPurchaseBankSlot(this.slotIndex)
        end

        SafeBankUpdateLayout()
    end)

    return btn
end

-- Create a single item slot button
local function CreateSlot(parent, id)
    local button = CreateFrame("Button", "SFramesBankSlot" .. id, parent, "ItemButtonTemplate")
    button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    button:RegisterForDrag("LeftButton")

    -- Rounded backdrop style (matching CharacterPanel equipment slots)
    local DEFAULT_BORDER = (_A and _A.slotBorder) or { 0.25, 0.25, 0.3, 0.8 }
    button:SetBackdrop({
        bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 18,
        insets = { left = 2, right = 2, top = 2, bottom = 2 }
    })
    button:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.9)
    button:SetBackdropBorderColor(DEFAULT_BORDER[1], DEFAULT_BORDER[2], DEFAULT_BORDER[3], DEFAULT_BORDER[4])

    -- Inset icon within the rounded border
    local icon = _G[button:GetName() .. "IconTexture"]
    if icon then
        icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        icon:ClearAllPoints()
        icon:SetPoint("TOPLEFT", button, "TOPLEFT", 4, -4)
        icon:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -4, 4)
    end

    local qualGlow = button:CreateTexture(nil, "OVERLAY")
    qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
    qualGlow:SetBlendMode("ADD")
    qualGlow:SetAlpha(0.8)
    qualGlow:SetWidth(SLOT_SIZE * 1.9)
    qualGlow:SetHeight(SLOT_SIZE * 1.9)
    qualGlow:SetPoint("CENTER", button, "CENTER", 0, 0)
    qualGlow:Hide()
    button.qualGlow = qualGlow

    function button:SetBorderColor(r, g, b, a)
        self.qualGlow:SetVertexColor(r, g, b)
        self.qualGlow:Show()
        self._qualityBorder = true
    end
    function button:ShowBorder()
        self._qualityBorder = true
    end
    function button:HideBorder()
        self.qualGlow:Hide()
        self._qualityBorder = false
    end

    -- Hide the ugly default rounded Blizzard border
    local nt = _G[button:GetName() .. "NormalTexture"]
    if nt then nt:SetTexture(nil) nt:Hide() end

    -- Grey item marker (a small coin/junk icon in the corner)
    local junkIcon = button:CreateTexture(nil, "OVERLAY")
    junkIcon:SetTexture("Interface\\Buttons\\UI-GroupLoot-Coin-Up")
    junkIcon:SetWidth(14)
    junkIcon:SetHeight(14)
    junkIcon:SetPoint("TOPLEFT", button, "TOPLEFT", 1, -1)
    junkIcon:Hide()
    button.junkIcon = junkIcon

    local previewGlow = button:CreateTexture(nil, "OVERLAY")
    previewGlow:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
    previewGlow:SetBlendMode("ADD")
    previewGlow:SetAllPoints(button)
    previewGlow:Hide()
    button.previewGlow = previewGlow

    button:SetScript("OnEnter", function()
        if this.bagID == nil or this.slotID == nil then return end
        GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
        GameTooltip:ClearLines()

        if SFrames.Bags.Bank.isOffline and SFrames.Bags.Bank.offlineChar then
            local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Bank.offlineChar)
            if data and data.bank and data.bank[this.bagID] and data.bank[this.bagID].items[this.slotID] then
                local link = data.bank[this.bagID].items[this.slotID].link
                local shown = false
                if link then
                    local _, _, itemStr = string.find(link, "(item:[%-?%d:]+)")
                    local ok = false
                    if itemStr then
                        ok = pcall(function() GameTooltip:SetHyperlink(itemStr) end)
                    else
                        ok = pcall(function() GameTooltip:SetHyperlink(link) end)
                    end
                    shown = ok and HasTooltipText()

                    if not shown then
                        local name = GetItemNameFromLink(link)
                        if name and name ~= "" then
                            GameTooltip:SetText(name, 1, 1, 1)
                            shown = true
                        end
                    end

                    if not shown then
                        GameTooltip:SetText(TEXT_ITEM, 1, 1, 1)
                    end
                else
                    GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65)
                end
            else
                GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65)
            end
        else
            local shown = SetTooltipFromBankContainerItem(this.bagID, this.slotID, this.itemLink, this.itemName)
            if not shown then
                GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65)
            end
        end
        if IsControlKeyDown() then
            ShowInspectCursor()
        end
        GameTooltip:Show()
    end)

    button:SetScript("OnUpdate", function()
        if GameTooltip:IsOwned(this) then
            if IsControlKeyDown() then
                if not this.controlDownLast then
                    this.controlDownLast = true
                    ShowInspectCursor()
                end
            else
                if this.controlDownLast then
                    this.controlDownLast = false
                    ResetCursor()
                end
            end
        end
    end)

    button:SetScript("OnLeave", function()
        GameTooltip:Hide()
        ResetCursor()
    end)

    local cooldown = CreateFrame("Model", button:GetName().."Cooldown", button, "CooldownFrameTemplate")
    cooldown:SetAllPoints(button)

    function button:SplitStack(split)
        if not split or split < 1 then return end
        if self.bagID == nil or self.slotID == nil then return end
        if self.bagID == -1 then
            SplitContainerItem(-1, self.slotID, split)
        else
            SplitContainerItem(self.bagID, self.slotID, split)
        end
    end

    button:SetScript("OnClick", function()
        local bagID  = this.bagID
        local slotID = this.slotID
        local isOffline = SFrames.Bags.Bank.isOffline

        -- Helper: get item link for this slot (works both online and offline)
        local function GetSlotLink()
            if isOffline and SFrames.Bags.Bank.offlineChar then
                local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Bank.offlineChar)
                if data and data.bank and data.bank[bagID] and data.bank[bagID].items[slotID] then
                    return data.bank[bagID].items[slotID].link
                end
                return nil
            end
            if bagID == -1 then
                return GetContainerItemLink(-1, slotID)
            else
                return GetContainerItemLink(bagID, slotID)
            end
        end

        if IsControlKeyDown() and arg1 == "LeftButton" then
            local link = GetSlotLink()
            if link and DressUpItemLink then
                DressUpItemLink(link)
                return
            end
        end
        if IsShiftKeyDown() then
            local eb = ChatFrameEditBox
            if eb and eb.IsVisible and eb:IsVisible() then
                local link = GetSlotLink()
                if link then eb:Insert(link) end
                return
            end
            if not isOffline and arg1 == "LeftButton" and (not CursorHasItem()) and OpenStackSplitFrame then
                local _, itemCount = GetContainerItemInfo(bagID, slotID)
                if itemCount and itemCount > 1 then
                    OpenStackSplitFrame(itemCount, this, "BOTTOMLEFT", "TOPLEFT")
                    return
                end
            end
        end

        -- Block all other actions in offline mode
        if isOffline then return end

        if arg1 == "RightButton" then
            if AutoStoreBankItem then
                local ok = pcall(function() AutoStoreBankItem(bagID, slotID) end)
                if ok then return end
            end
            UseContainerItem(bagID, slotID)
        else
            if bagID == -1 then
                PickupContainerItem(-1, slotID)
            else
                PickupContainerItem(bagID, slotID)
            end
        end
    end)

    button:SetScript("OnDragStart", function()
        if SFrames.Bags.Bank.isOffline then return end
        if CursorHasItem() then return end
        local bagID  = this.bagID
        local slotID = this.slotID
        if bagID == -1 then
            PickupContainerItem(-1, slotID)
        else
            PickupContainerItem(bagID, slotID)
        end
    end)

    button:SetScript("OnReceiveDrag", function()
        if SFrames.Bags.Bank.isOffline then return end
        if CursorHasItem() then
            local bagID  = this.bagID
            local slotID = this.slotID
            if bagID == -1 then
                PickupContainerItem(-1, slotID)
            else
                PickupContainerItem(bagID, slotID)
            end
        end
    end)

    return button
end

function SFrames.Bags.Bank:PreviewBankBagSlots(targetBagID)
    for _, btn in ipairs(ItemSlots) do
        if btn and btn:IsShown() then
            local icon = _G[btn:GetName() .. "IconTexture"]
            local isMatch = (btn.bagID == targetBagID)

            if icon then
                icon:SetVertexColor(1, 1, 1)
            end

            if btn.previewGlow then
                if isMatch then
                    btn.previewGlow:Show()
                else
                    btn.previewGlow:Hide()
                end
            end
        end
    end
end

function SFrames.Bags.Bank:ClearBankBagPreview()
    for _, btn in ipairs(ItemSlots) do
        if btn and btn:IsShown() then
            local icon = _G[btn:GetName() .. "IconTexture"]
            if icon then icon:SetVertexColor(1, 1, 1) end
            if btn.previewGlow then btn.previewGlow:Hide() end
        end
    end
end

function SFrames.Bags.Bank:UpdateBankBagButtons()
    if not SFBankFrame then return end

    local isOffline = self.isOffline
    local offlineDB = nil
    if isOffline and self.offlineChar and SFrames.Bags.Offline and SFrames.Bags.Offline.GetCharacterData then
        offlineDB = SFrames.Bags.Offline:GetCharacterData(self.offlineChar)
        if not offlineDB then
            isOffline = false
            self.isOffline = false
            self.offlineChar = nil
        end
    end

    local purchased = 0
    if isOffline and offlineDB then
        local _, _, _, _, offPurchased = GetOfflineBankSlotState(offlineDB, 1)
        purchased = offPurchased or 0
    else
        purchased = (GetNumBankSlots and GetNumBankSlots()) or 0
    end
    local nextSlot = purchased + 1
    local placeholderTex = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Bag"

    for i = 1, BANK_BAG_COUNT do
        local btn = BankBagButtons[i]
        if btn then
            local bagID = i + (BANK_BAG_FIRST_ID - 1)
            local unlocked = false
            local tex = nil

            if isOffline and offlineDB then
                local bagSlots, offlineLink, offlineTex
                unlocked, bagSlots, offlineLink, offlineTex = GetOfflineBankSlotState(offlineDB, i)

                if offlineLink then
                    tex = GetIconFromItemLink(offlineLink)
                end
                if (not tex) and offlineLink and IsUsableBankBagIconTexture(offlineTex) then
                    tex = offlineTex
                end
                if not tex then
                    tex = placeholderTex
                end
            else
                local purchasedSlot = (i <= purchased)
                local bagSlots = GetContainerNumSlots(bagID) or 0
                local invSlot = GetBankBagInvSlotID(i)
                local invLink = SafeGetInventoryItemLink("player", invSlot)
                local invTex = SafeGetInventoryItemTexture("player", invSlot)
                local liveTex = GetLiveBankBagIconTexture(i)

                if invLink and IsBagItemLink(invLink) then
                    tex = GetIconFromItemLink(invLink)
                    if (not tex) and IsUsableBankBagIconTexture(invTex) then
                        tex = invTex
                    end
                end
                if (not tex) and IsUsableBankBagIconTexture(liveTex) then
                    tex = liveTex
                end

                local hasBag = (tex ~= nil) or (bagSlots > 0)
                unlocked = purchasedSlot or hasBag

                if not tex then
                    tex = placeholderTex
                end
            end

            btn.unlocked = unlocked
            btn.icon:SetTexture(tex)
            btn:Enable()
            if btn.icon.SetDesaturated then
                pcall(function() btn.icon:SetDesaturated(false) end)
            end
            if btn.bg then btn.bg:SetTexture(0, 0, 0, 0) end
            btn:SetAlpha(1)
            btn.icon:SetAlpha(1)

            if unlocked then
                btn.icon:SetVertexColor(1, 1, 1, 1)
                btn.lockIcon:Hide()
            else
                btn.icon:SetVertexColor(0.5, 0.5, 0.5, 1)
                if (not isOffline) and i == nextSlot then
                    btn.lockIcon:Hide()
                else
                    btn.lockIcon:Show()
                end
            end

            btn:Show()
        end
    end

    if SFBankFrame.purchaseBtn then
        if isOffline then
            SFBankFrame.purchaseBtn:Hide()
        else
            if nextSlot <= BANK_BAG_COUNT then
                local cost = (GetBankSlotCost and GetBankSlotCost()) or 0
                SFBankFrame.purchaseBtn.cost = cost
                SFBankFrame.purchaseBtn:Show()
            else
                SFBankFrame.purchaseBtn:Hide()
            end
        end
    end
end

-- Build/Update the item slot grid
function SFrames.Bags.Bank:UpdateLayout()
    if not SFBankFrame then return end

    local cols = (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bankColumns) or 12
    local spacing = (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bankSpacing) or SPACING
    spacing = tonumber(spacing) or SPACING
    if spacing < 0 then spacing = 0 end
    local slots = {}

    local isOffline = self.isOffline
    local charName = self.offlineChar
    local offlineDB = nil
    if isOffline and charName then
        offlineDB = SFrames.Bags.Offline:GetCharacterData(charName)
        if not offlineDB then
            isOffline = false
            self.isOffline = false
            self.offlineChar = nil
            if SFBankFrame and SFBankFrame.RefreshCharacterSelectorText then
                SFBankFrame.RefreshCharacterSelectorText()
            end
        end
    end

    -- Update money display (online/offline)
    if SFBankFrame.moneyFrame then
        local copper = 0
        if isOffline and offlineDB then
            copper = offlineDB.money or 0
        else
            copper = GetMoney()
        end
        SetCoinDisplayMoney(SFBankFrame.moneyFrame, copper)
    end

    local bankBags = { -1 }
    for bag = BANK_BAG_FIRST_ID, BANK_BAG_LAST_ID do
        table.insert(bankBags, bag)
    end
    for _, bag in ipairs(bankBags) do
        local size = 0
        if isOffline and offlineDB then
            if offlineDB.bank and offlineDB.bank[bag] then size = offlineDB.bank[bag].size end
        else
            if bag == -1 then
                -- Try API first, fallback to hardcoded 24
                local apiSize = GetContainerNumSlots(-1)
                size = (apiSize and apiSize > 0) and apiSize or 24
            else
                size = GetContainerNumSlots(bag)
            end
        end
        for slot = 1, size do
            table.insert(slots, { bag = bag, slot = slot })
        end
    end

    -- Apply search filter (hide non-matching items, show dim-greyed slots instead)
    local searchFilter = bankSearchText or ""
    if searchFilter ~= "" then
        local filtered = {}
        for _, meta in ipairs(slots) do
            local link
            if isOffline and offlineDB then
                if offlineDB.bank and offlineDB.bank[meta.bag] and offlineDB.bank[meta.bag].items[meta.slot] then
                    link = offlineDB.bank[meta.bag].items[meta.slot].link
                end
            else
                if meta.bag == -1 then
                    link = GetContainerItemLink(-1, meta.slot)
                else
                    link = GetContainerItemLink(meta.bag, meta.slot)
                end
            end
            -- Keep slot if it matches OR if it's empty (so empty slots remain in grid)
            local keep = true
            if link then
                local name = GetItemInfo(link)
                if not name then
                    local _, _, parsedName = string.find(link, "%[(.+)%]")
                    name = parsedName
                end
                keep = TextMatchesSearch(name, searchFilter)
            end
            if keep then
                table.insert(filtered, meta)
            end
        end
        slots = filtered
    end

    local numSlots = table.getn(slots)
    if numSlots == 0 then numSlots = 1 end -- Prevent zero-size window
    local rows = math.ceil(numSlots / cols)

    -- Resize frame
    local width  = MARGIN * 2 + (cols * SLOT_SIZE) + math.max(0, (cols - 1)) * spacing
    local height = MARGIN * 2 + TOP_OFFSET + (rows * SLOT_SIZE) + math.max(0, (rows - 1)) * spacing + BOTTOM_OFFSET
    SFBankFrame:SetWidth(math.max(width, 160))
    SFBankFrame:SetHeight(height)

    -- Position & update slots
    for i, meta in ipairs(slots) do
        local btn = ItemSlots[i]
        if not btn then
            btn = CreateSlot(SFBankFrame, i)
            ItemSlots[i] = btn
        end

        btn.bagID  = meta.bag
        btn.slotID = meta.slot

        local row = math.floor((i - 1) / cols)
        local col = math.mod((i - 1), cols)
        btn:ClearAllPoints()
        btn:SetPoint("TOPLEFT", SFBankFrame, "TOPLEFT",
            MARGIN + col * (SLOT_SIZE + spacing),
            -(MARGIN + TOP_OFFSET + row * (SLOT_SIZE + spacing)))

        -- Fetch item info
        local texture, count, quality, link
        if isOffline and offlineDB then
            if offlineDB.bank and offlineDB.bank[meta.bag] and offlineDB.bank[meta.bag].items[meta.slot] then
                local item = offlineDB.bank[meta.bag].items[meta.slot]
                texture = item.texture
                count   = item.count
                quality = item.quality
                link    = item.link
            end
        else
            if meta.bag == -1 then
                -- Main bank slots: GetContainerItemInfo(-1, slot) is the correct API
                -- This requires the bank to already be open and data populated (0.5s delay ensures this)
                local t, c, _, q = GetContainerItemInfo(-1, meta.slot)
                texture = t; count = c; quality = q
                link    = GetContainerItemLink(-1, meta.slot)
            else
                local t, c, _, q = GetContainerItemInfo(meta.bag, meta.slot)
                texture = t; count = c; quality = q
                link = GetContainerItemLink(meta.bag, meta.slot)
            end
        end

        btn.itemLink = link
        if link then
            btn.itemName = GetItemInfo(link)
        else
            btn.itemName = nil
        end

        SetItemButtonTexture(btn, texture)
        SetItemButtonCount(btn, count)
        local iconTex = _G[btn:GetName() .. "IconTexture"]
        if iconTex then iconTex:SetVertexColor(1, 1, 1) end
        if btn.previewGlow then btn.previewGlow:Hide() end

        -- Quality border & Grey marker
        btn:HideBorder()
        btn.junkIcon:Hide()

        if link then
            -- Safest 1.12 generic approach: Read the color straight out of the hyperlink!
            local _, _, hex = string.find(link, "|c(%x+)|H")
            local parsedColor = false
            
            if hex and string.len(hex) == 8 then
                local hexLower = string.lower(hex)
                if hexLower == "ff9d9d9d" then
                    -- Poor / Grey Item
                    btn:SetBorderColor(0.5, 0.5, 0.5, 1)
                    btn:ShowBorder()
                    btn.junkIcon:Show()
                    parsedColor = true
                elseif hexLower == "ffffffff" then
                    -- Common / White Item
                    parsedColor = true
                else
                    -- Green, Blue, Purple, Orange
                    local r = tonumber(string.sub(hex, 3, 4), 16) / 255
                    local g = tonumber(string.sub(hex, 5, 6), 16) / 255
                    local b = tonumber(string.sub(hex, 7, 8), 16) / 255
                    btn:SetBorderColor(r, g, b, 1)
                    btn:ShowBorder()
                    parsedColor = true
                end
            end
            
            -- Fallback
            if not parsedColor then
                local q = quality
                if not q then
                    local _, _, itemString = string.find(link, "item:(%d+)")
                    if itemString then
                        local _, _, scanRarity = GetItemInfo("item:" .. itemString)
                        q = scanRarity
                    end
                end
                
                if q then
                    if q == 0 then
                        btn:SetBorderColor(0.5, 0.5, 0.5, 1)
                        btn:ShowBorder()
                        btn.junkIcon:Show()
                    elseif q > 1 then
                        local r, g, b = GetItemQualityColor(q)
                        btn:SetBorderColor(r, g, b, 1)
                        btn:ShowBorder()
                    end
                end
            end
        end

        -- Cooldowns
        local cooldown = _G[btn:GetName() .. "Cooldown"]
        if cooldown then
            if isOffline then
                cooldown:Hide()
            else
                local start, duration, enable = GetContainerItemCooldown(meta.bag, meta.slot)
                if start and duration and start > 0 and duration > 0 then
                    CooldownFrame_SetTimer(cooldown, start, duration, enable)
                    cooldown:Show()
                else
                    cooldown:Hide()
                end
            end
        end

        btn:Show()
    end

    -- Hide excess buttons
    for i = numSlots + 1, table.getn(ItemSlots) do
        if ItemSlots[i] then ItemSlots[i]:Hide() end
    end

    self:UpdateBankBagButtons()
end

-- Main frame initialization (called once after PLAYER_LOGIN)
function SFrames.Bags.Bank:Initialize()
    if SFBankFrame then return end

    SFBankFrame = CreateFrame("Frame", "SFramesBankFrame", UIParent)
    SFBankFrame:SetWidth(420)
    SFBankFrame:SetHeight(200)
    SFBankFrame:SetFrameStrata("HIGH")
    SFBankFrame:SetToplevel(true)
    SFBankFrame:EnableMouse(true)
    SFBankFrame:SetMovable(true)
    SFBankFrame:SetClampedToScreen(true)
    SFBankFrame:RegisterForDrag("LeftButton")
    SFBankFrame:SetScript("OnDragStart", function() this:StartMoving() end)
    SFBankFrame:SetScript("OnDragStop", function()
        this:StopMovingOrSizing()
        SaveBankFramePosition()
    end)
    ApplyBankFramePosition()
    tinsert(UISpecialFrames, "SFramesBankFrame")
    SFBankFrame:Hide()

    -- ESC menu style rounded backdrop
    SFBankFrame:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 14,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    local _A = SFrames.ActiveTheme
    if _A and _A.panelBg then
        SFBankFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], _A.panelBg[4] or 0.95)
        SFBankFrame:SetBackdropBorderColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], _A.panelBorder[4] or 0.9)
    else
        SFBankFrame:SetBackdropColor(0.12, 0.06, 0.10, 0.95)
        SFBankFrame:SetBackdropBorderColor(0.55, 0.30, 0.42, 0.9)
    end
    local bankShadow = CreateFrame("Frame", nil, SFBankFrame)
    bankShadow:SetPoint("TOPLEFT", SFBankFrame, "TOPLEFT", -5, 5)
    bankShadow:SetPoint("BOTTOMRIGHT", SFBankFrame, "BOTTOMRIGHT", 5, -5)
    bankShadow:SetFrameLevel(math.max(SFBankFrame:GetFrameLevel() - 1, 0))
    bankShadow:SetBackdrop({
        bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 16,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    bankShadow:SetBackdropColor(0, 0, 0, 0.55)
    bankShadow:SetBackdropBorderColor(0, 0, 0, 0.4)
    local scale = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.bankScale) == "number" and SFramesDB.Bags.bankScale) or 0.85
    SFBankFrame:SetScale(scale)
    local bankAlpha = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.bankAlpha) == "number" and SFramesDB.Bags.bankAlpha) or 1
    SFBankFrame:SetAlpha(bankAlpha)

    local bankTitleIco = SFrames:CreateIcon(SFBankFrame, "gold", 14)
    bankTitleIco:SetDrawLayer("OVERLAY")
    bankTitleIco:SetPoint("TOPLEFT", SFBankFrame, "TOPLEFT", 10, -7)
    bankTitleIco:SetVertexColor(_A.title[1], _A.title[2], _A.title[3])

    local titleFS = SFrames:CreateFontString(SFBankFrame, 12, "LEFT")
    titleFS:SetPoint("LEFT", bankTitleIco, "RIGHT", 4, 0)
    titleFS:SetText(TEXT_BANK_TITLE)
    titleFS:SetTextColor(_A.title[1], _A.title[2], _A.title[3])
    SFBankFrame.title = titleFS

    -- Close button
    local closeBtn = CreateFrame("Button", "SFramesBankClose", SFBankFrame, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", SFBankFrame, "TOPRIGHT", 0, 0)
    closeBtn:SetScript("OnClick", function() SFrames.Bags.Bank:Close() end)

    -- Money display
    SFBankFrame.moneyFrame = CreateCoinDisplay(SFBankFrame, "SFramesBankMoneyFrame")
    -- Temporary anchor; final anchor is set after purchase button is created.
    SFBankFrame.moneyFrame:SetPoint("RIGHT", SFBankFrame, "BOTTOMRIGHT", -8, 17)
    SetCoinDisplayMoney(SFBankFrame.moneyFrame, 0)

    -- Search bar (row 2: below title)
    local searchEB = CreateFrame("EditBox", "SFramesBankSearchBox", SFBankFrame, "InputBoxTemplate")
    searchEB:SetWidth(120)
    searchEB:SetHeight(18)
    searchEB:SetPoint("TOPLEFT", SFBankFrame, "TOPLEFT", 10, -27)
    searchEB:SetAutoFocus(false)
    searchEB:SetScript("OnEnterPressed", function() this:ClearFocus() end)
    searchEB:SetScript("OnEscapePressed", function()
        this:ClearFocus()
        this:SetText("")
        bankSearchText = ""
        SafeBankUpdateLayout()
    end)
    searchEB:SetScript("OnTextChanged", function()
        bankSearchText = this:GetText() or ""
        SafeBankUpdateLayout()
    end)
    SFBankFrame.searchEB = searchEB

    local function CreateHeaderIconButton(name, parent, iconPath)
        local btn = CreateFrame("Button", name, parent)
        btn:SetWidth(18)
        btn:SetHeight(18)
        btn:SetFrameStrata(parent:GetFrameStrata())
        btn:SetFrameLevel(parent:GetFrameLevel() + 45)

        btn:SetBackdrop({
            bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            tile = true, tileSize = 16, edgeSize = 12,
            insets = { left = 2, right = 2, top = 2, bottom = 2 }
        })
        btn:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.86)
        btn:SetBackdropBorderColor(_A.slotBorder[1], _A.slotBorder[2], _A.slotBorder[3], _A.slotBorder[4] or 0.8)

        local icon = btn:CreateTexture(nil, "ARTWORK")
        icon:SetTexture(iconPath or "Interface\\Icons\\INV_Misc_QuestionMark")
        icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2)
        icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2)
        icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        btn.icon = icon

        local hl = btn:CreateTexture(nil, "HIGHLIGHT")
        hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
        hl:SetBlendMode("ADD")
        hl:SetAllPoints(btn)

        return btn
    end

    -- Sort button (icon)
    local sortBtn = CreateHeaderIconButton("SFramesBankSortBtn", SFBankFrame, "Interface\\Icons\\INV_Misc_Note_05")
    sortBtn:SetPoint("LEFT", searchEB, "RIGHT", 6, 0)
    SFrames:SetIcon(sortBtn.icon, "gold")
    sortBtn:SetScript("OnEnter", function()
        GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
        GameTooltip:SetText(TEXT_SORT, 1, 1, 1)
        GameTooltip:Show()
    end)
    sortBtn:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    sortBtn:SetScript("OnClick", function()
        if SFrames.Bags.Sort and SFrames.Bags.Sort.StartBank then
            SFrames.Bags.Sort:StartBank()
            return
        end

        -- Fallback for environments with a native bank sort API.
        SortBank()

        local elapsed = 0
        local t = CreateFrame("Frame")
        t:SetScript("OnUpdate", function()
            elapsed = elapsed + arg1
            if elapsed >= 0.3 then
                this:SetScript("OnUpdate", nil)
                SafeBankUpdateLayout()
            end
        end)
    end)
    SFBankFrame.sortBtn = sortBtn

    -- Bank bag slots (equip/unequip + purchase flow)
    for i = 1, BANK_BAG_COUNT do
        local bagBtn = CreateBankBagButton(SFBankFrame, i)
        if i == 1 then
            bagBtn:SetPoint("BOTTOMLEFT", SFBankFrame, "BOTTOMLEFT", 8, 6)
        else
            bagBtn:SetPoint("LEFT", BankBagButtons[i - 1], "RIGHT", BANK_BAG_SPACING, 0)
        end
        BankBagButtons[i] = bagBtn
    end

    local purchaseBtn = CreateHeaderIconButton("SFramesBankPurchaseBtn", SFBankFrame, "Interface\\Icons\\INV_Misc_Coin_01")
    purchaseBtn:SetWidth(BANK_BAG_SIZE)
    purchaseBtn:SetHeight(BANK_BAG_SIZE)
    purchaseBtn:SetPoint("BOTTOMRIGHT", SFBankFrame, "BOTTOMRIGHT", -8, 6)
    purchaseBtn.cost = 0
    purchaseBtn:SetScript("OnEnter", function()
        GameTooltip:SetOwner(this, "ANCHOR_TOP")
        GameTooltip:ClearLines()
        GameTooltip:SetText(TEXT_BUY_SLOT, 1, 1, 1)
        if SFrames.Bags.Bank.isOffline then
            GameTooltip:AddLine(TEXT_UNAVAILABLE_OFFLINE, 0.75, 0.75, 0.75)
        else
            local cost = this.cost or 0
            if cost > 0 then
                GameTooltip:AddLine(GetSafeCoinText(cost), 1, 0.82, 0)
            end
            GameTooltip:AddLine(TEXT_CLICK_BUY, 0.75, 0.75, 0.75)
        end
        GameTooltip:Show()
    end)
    purchaseBtn:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    purchaseBtn:SetScript("OnClick", function()
        if SFrames.Bags.Bank.isOffline then return end
        if PurchaseSlot then
            pcall(function() PurchaseSlot() end)
            SafeBankUpdateLayout()
        end
    end)
    SFBankFrame.purchaseBtn = purchaseBtn

    -- Keep bank money display on the same bottom row as bank bag controls.
    if SFBankFrame.moneyFrame then
        SFBankFrame.moneyFrame:ClearAllPoints()
        SFBankFrame.moneyFrame:SetPoint("RIGHT", purchaseBtn, "LEFT", -6, 0)
    end

    -- Character selector: button-triggered dropdown (stable text, avoids UIDropDown label glitches)
    local function GetCurrentCharacterName()
        local live = UnitName("player")
        if type(live) == "string" and live ~= "" then
            return live
        end
        local cached = SFrames.Bags.Offline:GetCurrentPlayerName()
        if type(cached) == "string" and cached ~= "" then
            return cached
        end
        return TEXT_CHARACTER
    end

    local charBtn = CreateHeaderIconButton("SFramesBankCharBtn", SFBankFrame, CHARACTER_SELECTOR_ICON)
    charBtn:SetPoint("TOPRIGHT", SFBankFrame, "TOPRIGHT", -8, -26)
    SFBankFrame.charSelectBtn = charBtn

    local dd = CreateFrame("Frame", "SFramesBankOfflineDD", SFBankFrame, "UIDropDownMenuTemplate")
    dd:Hide()
    dd:SetPoint("TOPRIGHT", charBtn, "BOTTOMRIGHT", 0, 0)

    local function RefreshCharacterSelectorText()
        if not SFBankFrame or not SFBankFrame.charSelectBtn then return end
        if SFrames.Bags.Bank.isOffline and SFrames.Bags.Bank.offlineChar then
            SFBankFrame.charSelectorLabel = SFrames.Bags.Bank.offlineChar .. " (" .. TEXT_OFFLINE .. ")"
        else
            SFBankFrame.charSelectorLabel = GetCurrentCharacterName() .. " (" .. TEXT_ONLINE .. ")"
        end
    end
    SFBankFrame.RefreshCharacterSelectorText = RefreshCharacterSelectorText

    local function OnSelect()
        local char = this.value
        if char == "CURRENT" then
            SFrames.Bags.Bank.isOffline = false
            SFrames.Bags.Bank.offlineChar = nil
        else
            SFrames.Bags.Bank.isOffline = true
            SFrames.Bags.Bank.offlineChar = char
        end
        RefreshCharacterSelectorText()
        SafeBankUpdateLayout()
    end

    UIDropDownMenu_Initialize(dd, function()
        local info = UIDropDownMenu_CreateInfo and UIDropDownMenu_CreateInfo() or {}
        local currentName = GetCurrentCharacterName()
        info.text = currentName .. " (" .. TEXT_ONLINE .. ")"
        info.value = "CURRENT"
        info.func = OnSelect
        info.checked = (not SFrames.Bags.Bank.isOffline)
        UIDropDownMenu_AddButton(info)

        local chars = SFrames.Bags.Offline:GetCharacterList()
        table.sort(chars)
        for _, char in ipairs(chars) do
            if char ~= currentName then
                info = UIDropDownMenu_CreateInfo and UIDropDownMenu_CreateInfo() or {}
                info.text = char .. " (" .. TEXT_OFFLINE .. ")"
                info.value = char
                info.func = OnSelect
                info.checked = SFrames.Bags.Bank.isOffline and SFrames.Bags.Bank.offlineChar == char
                UIDropDownMenu_AddButton(info)
            end
        end
    end)

    charBtn:SetScript("OnClick", function()
        ToggleDropDownMenu(1, nil, dd, this, 0, 0)
    end)
    charBtn:SetScript("OnEnter", function()
        GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
        GameTooltip:SetText(SFBankFrame.charSelectorLabel or TEXT_CHARACTER, 1, 1, 1)
        GameTooltip:Show()
    end)
    charBtn:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    RefreshCharacterSelectorText()

    -- Use BANKFRAME_OPENED / CLOSED events (same as Guda)
    -- IMPORTANT: WoW 1.12 needs a short delay before GetContainerItemInfo(-1,slot)
    -- returns valid data after BANKFRAME_OPENED fires.
    local eventFrame = CreateFrame("Frame")
    eventFrame:RegisterEvent("BANKFRAME_OPENED")
    eventFrame:RegisterEvent("BANKFRAME_CLOSED")
    eventFrame:RegisterEvent("BAG_UPDATE")
    eventFrame:RegisterEvent("PLAYERBANKSLOTS_CHANGED")
    eventFrame:RegisterEvent("PLAYERBANKBAGSLOTS_CHANGED")
    eventFrame:RegisterEvent("PLAYER_MONEY")
    eventFrame:SetScript("OnEvent", function()
        if not (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.enable) then return end

        if event == "BANKFRAME_OPENED" then
            -- CRITICAL: Do NOT call BankFrame:Hide() here!
            -- BankFrame has an OnHide script that calls CloseBankFrame(), which
            -- immediately closes the bank session and makes GetContainerItemInfo(-1,slot) return nil.
            -- Instead, move BankFrame off-screen so the session stays open but it's invisible.
            local bf = _G["BankFrame"]
            if bf then
                bf:ClearAllPoints()
                bf:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -5000, 5000)
                bf:EnableMouse(false)  -- Prevent accidental clicks
            end
            -- Open once; Open() already performs a delayed refresh.
            SFrames.Bags.Bank:Open()

        elseif event == "BANKFRAME_CLOSED" then
            if not isClosing then
                SFrames.Bags.Bank:Close()
            end

        elseif event == "BAG_UPDATE" or event == "PLAYERBANKSLOTS_CHANGED" or event == "PLAYERBANKBAGSLOTS_CHANGED" or event == "PLAYER_MONEY" then
            if event == "PLAYERBANKBAGSLOTS_CHANGED" then
                bankBagInvSlotCache = {}
            elseif event == "BAG_UPDATE" and type(arg1) == "number" and arg1 >= BANK_BAG_FIRST_ID and arg1 <= BANK_BAG_LAST_ID then
                bankBagInvSlotCache = {}
            end
            if SFBankFrame:IsVisible() and not SFrames.Bags.Bank.isOffline then
                SafeBankUpdateLayout()
            end
        end
    end)
end

function SFrames.Bags.Bank:Open()
    if not SFBankFrame then return end
    self.isOffline = false
    self.offlineChar = nil
    bankBagInvSlotCache = {}
    if SFBankFrame and SFBankFrame.RefreshCharacterSelectorText then
        SFBankFrame.RefreshCharacterSelectorText()
    end

    -- Bank-open layout: bank on left, bags on right.
    if SFBankFrame then
        SFBankFrame:ClearAllPoints()
        SFBankFrame:SetPoint("CENTER", UIParent, "CENTER", -360, 0)
    end
    local bagFrame = _G["SFramesBagFrame"]
    if bagFrame then
        bagFrame:ClearAllPoints()
        bagFrame:SetPoint("CENTER", UIParent, "CENTER", 360, 0)
    end
    
    SFBankFrame:Show()

    local elapsed = 0
    local nextRefresh = 1
    local refreshPoints = { 0.06, 0.20, 0.45, 0.85, 1.30 }
    local refreshTimer = CreateFrame("Frame")
    refreshTimer:SetScript("OnUpdate", function()
        elapsed = elapsed + arg1
        if nextRefresh <= table.getn(refreshPoints) and elapsed >= refreshPoints[nextRefresh] then
            if SFBankFrame:IsVisible() and not SFrames.Bags.Bank.isOffline then
                SafeBankUpdateLayout()
            end
            nextRefresh = nextRefresh + 1
        end
        if nextRefresh > table.getn(refreshPoints) then
            this:SetScript("OnUpdate", nil)
        end
    end)

    -- Keep bank bag state synced: some clients delay slot/container updates.
    local syncFrame = CreateFrame("Frame")
    syncFrame.timer = 0
    syncFrame:SetScript("OnUpdate", function()
        if not (SFBankFrame and SFBankFrame:IsVisible()) then
            this.timer = 0
            return
        end
        if SFrames.Bags.Bank.isOffline then
            this.timer = 0
            return
        end

        this.timer = this.timer + (arg1 or 0)
        if this.timer >= 0.5 then
            this.timer = 0
            SFrames.Bags.Bank:UpdateBankBagButtons()
        end
    end)
end

function SFrames.Bags.Bank:OpenOffline(charName)
    if not SFBankFrame then
        if self.Initialize then
            self:Initialize()
        end
    end
    if not SFBankFrame then
        return false
    end

    local targetChar = nil
    if type(charName) == "string" and charName ~= "" then
        targetChar = charName
    elseif SFrames.Bags.Offline and SFrames.Bags.Offline.GetCurrentPlayerName then
        targetChar = SFrames.Bags.Offline:GetCurrentPlayerName()
    end

    if not targetChar or targetChar == "" then
        if SFrames and SFrames.Print then
            SFrames:Print(TEXT_UNAVAILABLE_OFFLINE)
        end
        return false
    end

    local data = nil
    if SFrames.Bags.Offline and SFrames.Bags.Offline.GetCharacterData then
        data = SFrames.Bags.Offline:GetCharacterData(targetChar)
    end
    if not data then
        if SFrames and SFrames.Print then
            SFrames:Print(TEXT_UNAVAILABLE_OFFLINE)
        end
        return false
    end

    self.isOffline = true
    self.offlineChar = targetChar
    bankBagInvSlotCache = {}

    if SFBankFrame.RefreshCharacterSelectorText then
        SFBankFrame.RefreshCharacterSelectorText()
    end

    -- Match live-bank layout so bag and bank are shown side by side.
    SFBankFrame:ClearAllPoints()
    SFBankFrame:SetPoint("CENTER", UIParent, "CENTER", -360, 0)
    local bagFrame = _G["SFramesBagFrame"]
    if bagFrame then
        bagFrame:ClearAllPoints()
        bagFrame:SetPoint("CENTER", UIParent, "CENTER", 360, 0)
    end

    SFBankFrame:Show()
    SafeBankUpdateLayout()
    return true
end

function SFrames.Bags.Bank:Close()
    if isClosing then return end -- Already closing, prevent recursion
    isClosing = true
    local wasOffline = self.isOffline
    if SFBankFrame then SFBankFrame:Hide() end
    -- Restore BankFrame so WoW's close sequence works properly,
    -- then let CloseBankFrame() trigger its own OnHide naturally.
    local bf = _G["BankFrame"]
    if bf then
        bf:EnableMouse(true)
        bf:ClearAllPoints()
        bf:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
    end
    if (not wasOffline) and CloseBankFrame then
        CloseBankFrame() -- Tell server to close bank session (triggers BankFrame OnHide and closes it)
    end
    isClosing = false
end

function SFrames.Bags.Bank:Toggle()
    if SFBankFrame and SFBankFrame:IsVisible() then
        self:Close()
    else
        self:Open()
    end
end

SLASH_NANAMIBANKDBG1 = "/nanamibankdbg"
SlashCmdList["NANAMIBANKDBG"] = function()
    if not (SFrames and SFrames.Print) then return end

    local purchased = (GetNumBankSlots and GetNumBankSlots()) or 0
    SFrames:Print("BankDbg purchased=" .. tostring(purchased))

    for i = 1, BANK_BAG_COUNT do
        local bagID = i + (BANK_BAG_FIRST_ID - 1)
        local bagSlots = GetContainerNumSlots(bagID) or 0
        local invSlot = GetBankBagInvSlotID(i)
        local link = invSlot and SafeGetInventoryItemLink("player", invSlot) or nil
        local isBag = link and IsBagItemLink(link)
        local liveTex = GetLiveBankBagIconTexture(i)
        local unlocked = IsBankBagSlotUnlocked(i)

        local mapBag1 = nil
        local mapIdx1 = nil
        if BankButtonIDToInvSlotID then
            local okA, resA = pcall(function() return BankButtonIDToInvSlotID(bagID, 1) end)
            if okA then mapBag1 = resA end
            local okB, resB = pcall(function() return BankButtonIDToInvSlotID(i, 1) end)
            if okB then mapIdx1 = resB end
        end

        local line =
            "slot" .. i ..
            " bagID=" .. tostring(bagID) ..
            " size=" .. tostring(bagSlots) ..
            " unlocked=" .. tostring(unlocked) ..
            " invSlot=" .. tostring(invSlot) ..
            " link=" .. tostring(link) ..
            " isBag=" .. tostring(isBag) ..
            " liveTex=" .. tostring(liveTex) ..
            " map(bag,1)=" .. tostring(mapBag1) ..
            " map(idx,1)=" .. tostring(mapIdx1)

        SFrames:Print(line)
    end
end
