local _, ns = ...

-- Stock-Retail facade for Memories of Azeroth. The public addon deliberately
-- uses only Blizzard's Spellbook visual language and client-shipped artwork.
local BlizzardSkin = {}
ns.SkinBlizzard = BlizzardSkin

local unpack = unpack
local WHITE = "Interface\\Buttons\\WHITE8X8"
local PANEL_BUTTON_UP = "Interface\\Buttons\\UI-Panel-Button-Up"
local PANEL_BUTTON_DOWN = "Interface\\Buttons\\UI-Panel-Button-Down"
local PANEL_BUTTON_DISABLED = "Interface\\Buttons\\UI-Panel-Button-Disabled"
local PANEL_BUTTON_HIGHLIGHT = "Interface\\Buttons\\UI-Panel-Button-Highlight"
local STOCK_PARCHMENT = "Interface\\AchievementFrame\\UI-Achievement-Parchment-Horizontal"
local FAVORITE_BOOKMARK_SHAPE = "Interface\\AddOns\\MemoriesOfAzeroth\\Media\\FavoriteBookmarkMask.tga"

local ATLAS = {
    page = "spellbook-background-evergreen-right",
    header = "spellbook-background-evergreen-header",
    topStreaks = "_UI-Frame-TopTileStreaks",
    ribbon = "spellbook-background-evergreen-ribbon",
    corner = "spellbook-corner-flipbook-evergreen",
    headingBackplate = "spellbook-list-backplate",
    headingDivider = "spellbook-divider",
    itemBackplate = "spellbook-item-backplate",
    tabLeft = "uiframe-tab-left",
    tabMiddle = "_uiframe-tab-center",
    tabRight = "uiframe-tab-right",
    tabActiveLeft = "uiframe-activetab-left",
    tabActiveMiddle = "_uiframe-activetab-center",
    tabActiveRight = "uiframe-activetab-right",
    searchLeft = "common-search-border-left",
    searchMiddle = "common-search-border-middle",
    searchRight = "common-search-border-right",
    favorite = "auctionhouse-icon-favorite",
    favoriteOff = "auctionhouse-icon-favorite-off",
}

-- The current Retail Spellbook is one 806 x 805 page below a 54-pixel attic.
-- These values are a layout contract, not a request to scale the atlas.
local LAYOUT = {
    windowWidth = 1043,
    windowHeight = 832,
    shellWidth = 809,
    chromeTop = 27,
    railX = 14,
    railTop = 42,
    railWidth = 220,
    railTabHeight = 32,
    railTabGap = -3,
    patchTabHeight = 22,
    pageX = 234,
    pageTop = 27,
    pageWidth = 806,
    pageHeight = 805,
    headerHeight = 54,
    contentLeft = 70,
    contentRight = 46,
    contentTop = 104,
    contentBottom = 58,
    cardColumns = 3,
    cardWidth = 220,
    cardHeight = 178,
    cardGapX = 15,
    cardGapY = 12,
    searchWidth = 300,
    searchHeight = 30,
}

local COLORS = {
    -- SPELLBOOK_FONT_COLOR is used dynamically when Retail exposes it.  This
    -- fallback matches the warm ink used by the Evergreen Spellbook page.
    text = { 0.20, 0.105, 0.035, 1 },
    muted = { 0.34, 0.235, 0.13, 0.86 },
    light = { 1, 1, 1, 1 },
    accent = { 1, 0.82, 0, 1 },
    warning = { 0.72, 0.38, 0.08, 1 },
    danger = { 0.78, 0.16, 0.12, 1 },
    panel = { 0.16, 0.085, 0.035, 0.08 },
    inset = { 0.055, 0.032, 0.018, 0.78 },
    thumbnail = { 0.025, 0.018, 0.012, 1 },
    faint = { 0.25, 0.14, 0.06, 0.24 },
    border = { 0.48, 0.31, 0.13, 0.72 },
    earFill = { 31 / 255, 29 / 255, 24 / 255, 1 },
    hover = { 1, 0.82, 0, 0.16 },
}

BlizzardSkin.COLORS = COLORS

local FONT_OBJECTS = {
    heading = "SystemFont_Huge2",
    title = "SystemFont_Large",
    meta = "SystemFont_Med1",
    page = "SystemFont_Med3",
    button = "GameFontNormalSmall",
    buttonSelected = "GameFontHighlightSmall",
}

local styled = setmetatable({}, { __mode = "k" })

local function IsSecret(value)
    if not _G.issecretvalue then return false end
    local ok, secret = pcall(_G.issecretvalue, value)
    return ok and secret == true
end

local function SafeNumber(value, fallback)
    if IsSecret(value) or type(value) ~= "number" then return fallback end
    return value
end

local function IsEnabled(button)
    if not button or type(button.IsEnabled) ~= "function" then return true end
    local ok, enabled = pcall(button.IsEnabled, button)
    return ok and not IsSecret(enabled) and enabled ~= false
end

local function CanMutate(frame)
    if not frame then return false end
    if type(_G.InCombatLockdown) ~= "function" or not _G.InCombatLockdown() then return true end
    local object = frame
    for _ = 1, 4 do
        if type(object.IsProtected) == "function" then
            local ok, protected = pcall(object.IsProtected, object)
            if ok and not IsSecret(protected) and protected == true then return false end
        end
        if type(object.GetParent) ~= "function" then break end
        local ok, parent = pcall(object.GetParent, object)
        if not ok or not parent or IsSecret(parent) then break end
        object = parent
    end
    return true
end

local function SetColor(texture, color)
    if texture and type(texture.SetColorTexture) == "function" then
        texture:SetColorTexture(color[1], color[2], color[3], color[4] or 1)
    end
end

local function SetVertexColor(region, color)
    if region and type(region.SetVertexColor) == "function" then
        region:SetVertexColor(color[1], color[2], color[3], color[4] or 1)
    end
end

local function GetSpellbookInk()
    local color = _G.SPELLBOOK_FONT_COLOR
    if color and type(color.GetRGBA) == "function" then
        local ok, r, g, b, a = pcall(color.GetRGBA, color)
        if ok and not IsSecret(r) and not IsSecret(g) and not IsSecret(b) and not IsSecret(a)
            and type(r) == "number" and type(g) == "number" and type(b) == "number" then
            return { r, g, b, type(a) == "number" and a or 1 }
        end
    end
    return COLORS.text
end

local function CopyTable(source)
    local copy = {}
    for key, value in pairs(source) do copy[key] = value end
    return copy
end

local atlasAvailability = {}
local function AtlasAvailable(atlas)
    if atlasAvailability[atlas] ~= nil then return atlasAvailability[atlas] end
    if not _G.C_Texture or type(_G.C_Texture.GetAtlasInfo) ~= "function" then return false end
    local ok, info = pcall(_G.C_Texture.GetAtlasInfo, atlas)
    local available = ok and info ~= nil and not IsSecret(info)
    atlasAvailability[atlas] = available
    return available
end

local function SetAtlasSafe(texture, atlas, useAtlasSize)
    if not texture or type(texture.SetAtlas) ~= "function" or not AtlasAvailable(atlas) then return false end
    return pcall(texture.SetAtlas, texture, atlas, useAtlasSize == true)
end

local function SetQuarterTurnedAtlasInterior(texture, atlas, insetPixels)
    if not SetAtlasSafe(texture, atlas, false) then return false end
    local ok, info = pcall(_G.C_Texture.GetAtlasInfo, atlas)
    if not ok or type(info) ~= "table" or IsSecret(info) then return false end
    local left, right = info.leftTexCoord, info.rightTexCoord
    local top, bottom = info.topTexCoord, info.bottomTexCoord
    local sourceHeight = SafeNumber(info.height, 32)
    if type(left) ~= "number" or type(right) ~= "number"
        or type(top) ~= "number" or type(bottom) ~= "number" then return false end
    local inset = (bottom - top) * math.max(0, insetPixels or 0) / math.max(1, sourceHeight)
    top, bottom = top + inset, bottom - inset
    -- Destination UL/LL/UR/LR sample source BL/BR/TL/TR. This rotates the
    -- native vertical tab gradient clockwise into a left-to-right gradient.
    texture:SetTexCoord(left, bottom, right, bottom, left, top, right, top)
    return true
end

local function CreateNativeAtlas(owner, layer, sublevel, atlas)
    local texture = owner:CreateTexture(nil, layer or "ARTWORK", nil, sublevel)
    SetAtlasSafe(texture, atlas, true)
    if texture.SetSnapToPixelGrid then texture:SetSnapToPixelGrid(false) end
    if texture.SetTexelSnappingBias then texture:SetTexelSnappingBias(0) end
    return texture
end

local function TintNineSlice(panel, color)
    if not panel then return end
    for _, key in ipairs({
        "TopLeftCorner", "TopRightCorner", "BottomLeftCorner", "BottomRightCorner",
        "TopEdge", "BottomEdge", "LeftEdge", "RightEdge", "Center",
    }) do
        SetVertexColor(panel[key], color)
    end
end

local function ApplyNineSlice(owner, layoutName, storageKey)
    if not owner or not CanMutate(owner) then return nil end
    storageKey = storageKey or "_cutsceneBlizzardNineSlice"
    if owner[storageKey] then return owner[storageKey] end
    if type(_G.NineSliceUtil) ~= "table" then return nil end

    local panel = CreateFrame("Frame", nil, owner, "NineSlicePanelTemplate")
    panel:SetAllPoints(owner)
    if type(panel.SetFrameLevel) == "function" and type(owner.GetFrameLevel) == "function" then
        panel:SetFrameLevel(SafeNumber(owner:GetFrameLevel(), 0) + 1)
    end
    local ok
    if type(_G.NineSliceUtil.ApplyLayoutByName) == "function" then
        ok = pcall(_G.NineSliceUtil.ApplyLayoutByName, panel, layoutName)
    elseif type(_G.NineSliceUtil.ApplyLayout) == "function" and type(_G.NineSliceLayouts) == "table" then
        local layout = _G.NineSliceLayouts[layoutName]
        if layout then ok = pcall(_G.NineSliceUtil.ApplyLayout, panel, layout) end
    end
    if not ok then panel:Hide(); return nil end
    owner[storageKey] = panel
    return panel
end

local function ApplyFont(fontString, token)
    local object = _G[FONT_OBJECTS[token or ""] or ""]
    if fontString and object and type(fontString.SetFontObject) == "function" then
        fontString:SetFontObject(object)
    end
end

local function CreatePanelButtonArt(button)
    local art = {}
    art.left = button:CreateTexture(nil, "BACKGROUND")
    art.left:SetSize(12, 22)
    art.left:SetPoint("LEFT")
    art.left:SetTexCoord(0, 0.09375, 0, 0.6875)
    art.right = button:CreateTexture(nil, "BACKGROUND")
    art.right:SetSize(12, 22)
    art.right:SetPoint("RIGHT")
    art.right:SetTexCoord(0.53125, 0.625, 0, 0.6875)
    art.middle = button:CreateTexture(nil, "BACKGROUND")
    art.middle:SetPoint("TOPLEFT", art.left, "TOPRIGHT")
    art.middle:SetPoint("BOTTOMRIGHT", art.right, "BOTTOMLEFT")
    art.middle:SetTexCoord(0.09375, 0.53125, 0, 0.6875)
    art.highlight = button:CreateTexture(nil, "HIGHLIGHT")
    art.highlight:SetAllPoints(button)
    art.highlight:SetTexture(PANEL_BUTTON_HIGHLIGHT)
    art.highlight:SetTexCoord(0, 0.625, 0, 0.6875)
    art.highlight:SetBlendMode("ADD")
    return art
end

local function SetPanelButtonArt(art, texture)
    if not art then return end
    art.left:SetTexture(texture)
    art.middle:SetTexture(texture)
    art.right:SetTexture(texture)
end

local function SetPanelButtonShown(art, shown)
    if not art then return end
    art.left:SetShown(shown)
    art.middle:SetShown(shown)
    art.right:SetShown(shown)
    art.highlight:SetShown(shown)
end

local function CreateTabArt(button)
    local art = {}
    local function Piece(key, atlas, point, relative, relativePoint, x)
        local texture = CreateNativeAtlas(button, "BACKGROUND", nil, atlas)
        if relative then texture:SetPoint(point, relative, relativePoint, x or 0, 0)
        else texture:SetPoint(point, button, point, x or 0, 0) end
        art[key] = texture
        return texture
    end

    local left = Piece("left", ATLAS.tabLeft, "TOPLEFT")
    local right = Piece("right", ATLAS.tabRight, "TOPRIGHT", button, "TOPRIGHT", 6)
    local middle = Piece("middle", ATLAS.tabMiddle, "LEFT", left, "RIGHT")
    middle:ClearAllPoints()
    middle:SetPoint("LEFT", left, "RIGHT")
    middle:SetPoint("RIGHT", right, "LEFT")
    middle:SetHorizTile(true)

    local activeLeft = Piece("activeLeft", ATLAS.tabActiveLeft, "TOPLEFT")
    local activeRight = Piece("activeRight", ATLAS.tabActiveRight, "TOPRIGHT", button, "TOPRIGHT", 7)
    local activeMiddle = Piece("activeMiddle", ATLAS.tabActiveMiddle, "LEFT", activeLeft, "RIGHT")
    activeMiddle:ClearAllPoints()
    activeMiddle:SetPoint("LEFT", activeLeft, "RIGHT")
    activeMiddle:SetPoint("RIGHT", activeRight, "LEFT")
    activeMiddle:SetHorizTile(true)

    art.highlightLeft = CreateNativeAtlas(button, "HIGHLIGHT", nil, ATLAS.tabLeft)
    art.highlightLeft:SetPoint("TOPLEFT", left)
    art.highlightMiddle = CreateNativeAtlas(button, "HIGHLIGHT", nil, ATLAS.tabMiddle)
    art.highlightMiddle:SetPoint("LEFT", middle, "LEFT")
    art.highlightMiddle:SetPoint("RIGHT", middle, "RIGHT")
    art.highlightMiddle:SetHorizTile(true)
    art.highlightRight = CreateNativeAtlas(button, "HIGHLIGHT", nil, ATLAS.tabRight)
    art.highlightRight:SetPoint("TOPRIGHT", right)
    for _, texture in ipairs({ art.highlightLeft, art.highlightMiddle, art.highlightRight }) do
        texture:SetBlendMode("ADD")
        texture:SetAlpha(0.4)
    end
    return art
end

local function SetTabSelected(art, selected)
    if not art then return end
    if art.isSideEar and art.sideNormalFill then
        for _, texture in ipairs({
            art.left, art.middle, art.right,
            art.activeLeft, art.activeMiddle, art.activeRight,
            art.highlightLeft, art.highlightMiddle, art.highlightRight,
        }) do
            texture:Hide()
        end
        art.sideNormalFill:SetShown(not selected)
        art.sideActiveFill:SetShown(selected)
        local color = selected and COLORS.accent or COLORS.border
        for _, line in ipairs({ art.sideLeft, art.sideTop, art.sideBottom }) do
            line:SetVertexColor(color[1], color[2], color[3], selected and 0.96 or 0.82)
        end
        return
    end
    art.left:SetShown(not selected)
    art.middle:SetShown(not selected)
    art.right:SetShown(not selected and not art.isSideEar)
    art.activeLeft:SetShown(selected)
    art.activeMiddle:SetShown(selected)
    art.activeRight:SetShown(selected and not art.isSideEar)
end

local function ConfigureSideEarArt(button, art)
    if not art or art.isSideEar then return end
    art.isSideEar = true
    art.sideNormalFill = button:CreateTexture(nil, "BACKGROUND", nil, -1)
    art.sideNormalFill:SetPoint("TOPLEFT", button, "TOPLEFT", 2, -2)
    art.sideNormalFill:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 8, 2)
    SetQuarterTurnedAtlasInterior(art.sideNormalFill, ATLAS.tabMiddle, 2)

    art.sideActiveFill = button:CreateTexture(nil, "BACKGROUND", nil, -1)
    art.sideActiveFill:SetPoint("TOPLEFT", button, "TOPLEFT", 2, -2)
    art.sideActiveFill:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 8, 2)
    SetQuarterTurnedAtlasInterior(art.sideActiveFill, ATLAS.tabActiveMiddle, 2)

    art.sideLeft = button:CreateTexture(nil, "ARTWORK")
    art.sideLeft:SetTexture(WHITE)
    art.sideLeft:SetPoint("TOPLEFT")
    art.sideLeft:SetPoint("BOTTOMLEFT")
    art.sideLeft:SetWidth(2)

    art.sideTop = button:CreateTexture(nil, "ARTWORK")
    art.sideTop:SetTexture(WHITE)
    art.sideTop:SetPoint("TOPLEFT")
    art.sideTop:SetPoint("TOPRIGHT", button, "TOPRIGHT", 8, 0)
    art.sideTop:SetHeight(2)

    art.sideBottom = button:CreateTexture(nil, "ARTWORK")
    art.sideBottom:SetTexture(WHITE)
    art.sideBottom:SetPoint("BOTTOMLEFT")
    art.sideBottom:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 8, 0)
    art.sideBottom:SetHeight(2)

    art.sideHighlight = button:CreateTexture(nil, "HIGHLIGHT")
    art.sideHighlight:SetTexture(WHITE)
    art.sideHighlight:SetPoint("TOPLEFT", 2, -2)
    art.sideHighlight:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 8, 2)
    art.sideHighlight:SetVertexColor(1, 0.82, 0.18, 0.12)
    art.sideHighlight:SetBlendMode("ADD")
end

local function ConfigureTopTabArt(art)
    if not art or art.isTop then return end
    art.isTop = true
    for _, texture in ipairs({
        art.left, art.middle, art.right,
        art.activeLeft, art.activeMiddle, art.activeRight,
        art.highlightLeft, art.highlightMiddle, art.highlightRight,
    }) do
        texture:ClearAllPoints()
        if texture.SetRotation then texture:SetRotation(math.pi) end
    end

    -- The stock caps are generous enough for full text tabs, but overlap when
    -- version-only tabs become compact. Narrow the cap regions—not the whole
    -- button—so their shadows stay clean while the row remains space-efficient.
    for _, cap in ipairs({
        art.left, art.right, art.activeLeft, art.activeRight,
        art.highlightLeft, art.highlightRight,
    }) do
        cap:SetWidth(22)
    end

    art.activeRight:SetPoint("BOTTOMLEFT", art.activeRight:GetParent(), "BOTTOMLEFT", -7, 0)
    art.activeLeft:SetPoint("BOTTOMRIGHT")
    art.activeMiddle:SetPoint("LEFT", art.activeRight, "RIGHT")
    art.activeMiddle:SetPoint("RIGHT", art.activeLeft, "LEFT")

    art.right:SetPoint("BOTTOMLEFT", art.right:GetParent(), "BOTTOMLEFT", -6, 0)
    art.left:SetPoint("BOTTOMRIGHT")
    art.middle:SetPoint("LEFT", art.right, "RIGHT")
    art.middle:SetPoint("RIGHT", art.left, "LEFT")

    art.highlightLeft:SetPoint("TOPRIGHT", art.left)
    art.highlightRight:SetPoint("TOPLEFT", art.right)
    art.highlightMiddle:SetPoint("LEFT", art.middle, "LEFT")
    art.highlightMiddle:SetPoint("RIGHT", art.middle, "RIGHT")
end

local function RefreshButton(button, state)
    if not state or not CanMutate(button) then return end
    local enabled = IsEnabled(button)
    if state.variant == "tab" then
        SetPanelButtonShown(state.panelArt, false)
        SetTabSelected(state.tabArt, state.selected == true)
        if state.label then
            ApplyFont(state.label, state.selected and "buttonSelected" or "button")
            if state.isTopTab then
                local y = state.selected and 0 or -3
                state.label:ClearAllPoints()
                state.label:SetPoint("LEFT", button, "LEFT", 6, y)
                state.label:SetPoint("RIGHT", button, "RIGHT", -6, y)
            end
            state.label:SetTextColor(unpack(state.selected and COLORS.light or COLORS.accent))
        end
    elseif state.variant == "close" then
        SetPanelButtonShown(state.panelArt, false)
    else
        SetPanelButtonShown(state.panelArt, true)
        SetPanelButtonArt(state.panelArt, not enabled and PANEL_BUTTON_DISABLED or PANEL_BUTTON_UP)
        if state.label then
            ApplyFont(state.label, "button")
            local color = not enabled and COLORS.muted or (state.selected and COLORS.light or COLORS.text)
            state.label:SetTextColor(unpack(color))
        end
    end
end

local function RefreshObject(object, state)
    if not state or not CanMutate(object) then return end
    if state.kind == "button" then
        RefreshButton(object, state)
    elseif state.kind == "panel" then
        SetColor(state.fill, COLORS[state.role] or COLORS.panel)
        TintNineSlice(state.nineSlice, COLORS.border)
    elseif state.kind == "text" then
        local color = state.role == "text" and GetSpellbookInk() or (COLORS[state.role] or GetSpellbookInk())
        object:SetTextColor(unpack(color))
        if state.fontToken then ApplyFont(object, state.fontToken) end
    elseif state.kind == "edit" then
        local color = state.focused and COLORS.accent or { 1, 1, 1, 1 }
        SetVertexColor(state.left, color)
        SetVertexColor(state.middle, color)
        SetVertexColor(state.right, color)
    elseif state.kind == "scrollbar" then
        SetColor(state.track, COLORS.faint)
        local earFill = COLORS.earFill
        SetColor(state.thumb, earFill)
        if state.glow then
            SetColor(state.glow, { earFill[1], earFill[2], earFill[3], 0.26 })
        end
    elseif state.kind == "thumbnail" then
        SetColor(state.fill, COLORS.thumbnail)
        TintNineSlice(state.nineSlice, COLORS.border)
    end
end

function BlizzardSkin:GetThemeID() return "blizzard" end
function BlizzardSkin:GetThemeName() return "Blizzard Spellbook" end
function BlizzardSkin:GetPalette() return COLORS end
function BlizzardSkin:IsAvailable()
    return AtlasAvailable(ATLAS.page) and AtlasAvailable(ATLAS.header)
        and AtlasAvailable(ATLAS.tabLeft) and AtlasAvailable(ATLAS.tabActiveLeft)
end
function BlizzardSkin:GetLayoutMetrics() return CopyTable(LAYOUT) end
function BlizzardSkin:GetNativePageSize() return LAYOUT.pageWidth, LAYOUT.pageHeight end
function BlizzardSkin:GetFontObject(role) return _G[FONT_OBJECTS[role or ""] or ""] end

function BlizzardSkin:SetTheme()
    for object, state in pairs(styled) do RefreshObject(object, state) end
    return "blizzard"
end

function BlizzardSkin:NextTheme() return self:SetTheme() end

function BlizzardSkin:GetFallbackThumbnail()
    return STOCK_PARCHMENT
end

function BlizzardSkin:GetPaperTexture()
    return STOCK_PARCHMENT
end

function BlizzardSkin:GetBookmarkTexture()
    return nil
end

function BlizzardSkin:HasExpansionEar()
    return false
end

function BlizzardSkin:GetExpansionEarTexture()
    return nil
end

function BlizzardSkin:Shell(frame)
    if not frame or not CanMutate(frame) then return nil end
    local existing = styled[frame]
    if existing then return existing end
    if frame.SetBackdrop then frame:SetBackdrop(nil) end
    local state = { kind = "shell" }
    -- PlayerSpellsFrame inherits PortraitFrameTemplate. When UI.lua supplies
    -- that exact template, keep its native background, streaks, and NineSlice
    -- instead of recreating them with guessed offsets.
    state.nineSlice = frame.NineSlice
    if not state.nineSlice then
        state.nineSlice = ApplyNineSlice(frame, "PortraitFrameTemplate", "_cutsceneBlizzardShellBorder")
    else
        frame._cutsceneBlizzardShellBorder = state.nineSlice
    end
    styled[frame] = state
    return state
end

local function UpdateHeaderCrop(frame, texture)
    if not frame or not texture then return end
    local width = SafeNumber(frame:GetWidth(), LAYOUT.pageWidth)
    width = math.max(1, math.min(1612, width))
    texture:ClearAllPoints()
    texture:SetPoint("TOPLEFT")
    texture:SetSize(width, LAYOUT.headerHeight)
    -- This is the same crop Blizzard applies when its two-page header is
    -- minimized.  The ratio keeps one source texel per rendered texel.
    texture:SetTexCoord(0, width / 1614, 0, 1)
end

function BlizzardSkin:HeaderStrip(frame)
    if not frame or not CanMutate(frame) then return nil end
    if frame._cutsceneBlizzardHeaderStrip then return frame._cutsceneBlizzardHeaderStrip end
    if frame.SetBackdrop then frame:SetBackdrop(nil) end
    if frame.SetClipsChildren then frame:SetClipsChildren(true) end
    local strip = frame:CreateTexture(nil, "BACKGROUND", nil, -8)
    SetAtlasSafe(strip, ATLAS.header, false)
    UpdateHeaderCrop(frame, strip)
    frame._cutsceneBlizzardHeaderStrip = strip
    frame:HookScript("OnSizeChanged", function(active)
        UpdateHeaderCrop(active, active._cutsceneBlizzardHeaderStrip)
    end)
    return strip
end

function BlizzardSkin:JournalPage(frame)
    if not frame or not CanMutate(frame) then return nil end
    if frame._cutsceneBlizzardJournalPage then return frame._cutsceneBlizzardJournalPage end
    if frame.SetBackdrop then frame:SetBackdrop(nil) end
    if frame.SetClipsChildren then frame:SetClipsChildren(true) end
    local page = CreateNativeAtlas(frame, "BACKGROUND", -8, ATLAS.page)
    page:SetPoint("TOPLEFT")
    local corner = frame:CreateTexture(nil, "BACKGROUND", nil, -6)
    corner:SetSize(150, 155)
    corner:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -15, 6)
    SetAtlasSafe(corner, ATLAS.corner, false)
    -- Match Blizzard exactly: the atlas is an eight-frame 4 x 2 flipbook.
    -- Retail starts the animation and immediately pauses it so the texture sits
    -- on one correctly isolated frame while idle; manually subdividing the
    -- atlas can leak neighbouring sprite cells because of atlas padding.
    local cornerAnimGroup = corner:CreateAnimationGroup()
    local cornerFlipbook = cornerAnimGroup:CreateAnimation("FlipBook")
    cornerFlipbook:SetFlipBookRows(2)
    cornerFlipbook:SetFlipBookColumns(4)
    cornerFlipbook:SetFlipBookFrames(8)
    cornerFlipbook:SetDuration(0.25)
    cornerAnimGroup:Play()
    cornerAnimGroup:Pause()
    frame._cutsceneBlizzardBookCornerAnim = cornerAnimGroup
    frame._cutsceneBlizzardBookCorner = corner
    frame._cutsceneBlizzardJournalPage = page
    return page
end

function BlizzardSkin:Surface(frame)
    return self:JournalPage(frame)
end

function BlizzardSkin:PageHeading(frame, fontString)
    if not frame or not fontString or not CanMutate(frame) then return nil end
    local state = frame._cutsceneBlizzardPageHeading
    if not state then
        state = {}
        state.backplate = CreateNativeAtlas(frame, "BACKGROUND", -1, ATLAS.headingBackplate)
        state.backplate:SetAlpha(0.65)
        state.backplate:SetPoint("LEFT", fontString, "LEFT", -85, 10)
        state.divider = CreateNativeAtlas(frame, "ARTWORK", nil, ATLAS.headingDivider)
        state.divider:SetPoint("TOPLEFT", fontString, "BOTTOMLEFT", -32, -28)
        frame._cutsceneBlizzardPageHeading = state
    end
    ApplyFont(fontString, "heading")
    fontString:SetTextColor(unpack(GetSpellbookInk()))
    return state
end

function BlizzardSkin:PageDivider(texture)
    if not texture or not CanMutate(texture) then return nil end
    texture:ClearAllPoints()
    SetAtlasSafe(texture, ATLAS.headingDivider, true)
    return texture
end

function BlizzardSkin:Panel(frame, role)
    if not frame or not CanMutate(frame) then return nil end
    local existing = styled[frame]
    if existing then return existing end
    if frame.SetBackdrop then frame:SetBackdrop(nil) end
    local state = { kind = "panel", role = role or "panel" }
    state.fill = frame:CreateTexture(nil, "BACKGROUND", nil, -3)
    state.fill:SetAllPoints(frame)
    state.nineSlice = ApplyNineSlice(frame, "InsetFrameTemplate", "_cutsceneBlizzardPanelBorder")
    styled[frame] = state
    RefreshObject(frame, state)
    return state
end

function BlizzardSkin:PaperCard(frame)
    if not frame or not CanMutate(frame) then return nil end
    local state = styled[frame]
    if not state or state.kind ~= "panel" then
        if frame.SetBackdrop then frame:SetBackdrop(nil) end
        state = { kind = "panel", role = "card" }
        state.hover = frame:CreateTexture(nil, "BACKGROUND", nil, -1)
        state.hover:SetAllPoints(frame)
        SetColor(state.hover, COLORS.hover)
        state.hover:Hide()
        styled[frame] = state
    end
    if not state then return nil end
    if not state.itemBackplate then
        state.itemBackplate = CreateNativeAtlas(frame, "BACKGROUND", -2, ATLAS.itemBackplate)
        state.itemBackplate:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", -5, -2)
        state.itemBackplate:SetAlpha(0.25)
    end
    return state
end

function BlizzardSkin:Button(button, primary)
    if not button or not CanMutate(button) then return nil end
    local existing = styled[button]
    if existing then return existing end
    if button.SetBackdrop then button:SetBackdrop(nil) end
    local state = {
        kind = "button",
        variant = primary and "primary" or "panel",
        label = button.GetFontString and button:GetFontString() or nil,
    }
    state.panelArt = CreatePanelButtonArt(button)
    styled[button] = state
    RefreshButton(button, state)
    button:HookScript("OnMouseDown", function(active)
        local activeState = styled[active]
        if CanMutate(active) and activeState and activeState.variant ~= "tab"
            and activeState.variant ~= "close" and IsEnabled(active) then
            SetPanelButtonArt(activeState.panelArt, PANEL_BUTTON_DOWN)
        end
    end)
    button:HookScript("OnMouseUp", function(active) RefreshButton(active, styled[active]) end)
    button:HookScript("OnShow", function(active) RefreshButton(active, styled[active]) end)
    button:HookScript("OnEnable", function(active) RefreshButton(active, styled[active]) end)
    button:HookScript("OnDisable", function(active) RefreshButton(active, styled[active]) end)
    return state
end

function BlizzardSkin:Bookmark(button, _, selected, expansionKey)
    local state = self:Button(button)
    if not state or not CanMutate(button) then return nil end
    state.variant = "tab"
    state.expansionKey = expansionKey
    state.isTopTab = type(expansionKey) == "string" and expansionKey:find("patch:", 1, true) == 1
    state.isSideEar = not state.isTopTab
    state.selected = selected == true
    if not state.tabArt then state.tabArt = CreateTabArt(button) end
    if state.isTopTab then ConfigureTopTabArt(state.tabArt) end
    if state.isSideEar then ConfigureSideEarArt(button, state.tabArt) end
    RefreshButton(button, state)
    return state
end

function BlizzardSkin:SetButtonSelected(button, selected)
    local state = styled[button]
    if not state or state.kind ~= "button" then return end
    state.selected = selected == true
    RefreshButton(button, state)
end

function BlizzardSkin:AddButtonMark(button)
    local state = self:Button(button)
    if not state then return nil end
    if state.mark then return state.mark end
    state.mark = button:CreateTexture(nil, "OVERLAY")
    SetAtlasSafe(state.mark, ATLAS.ribbon, true)
    state.mark:SetPoint("RIGHT", button, "RIGHT", 8, -13)
    return state.mark
end

function BlizzardSkin:CloseButton(button)
    local state = self:Button(button)
    if not state or not CanMutate(button) then return nil end
    state.variant = "close"
    button:SetText("")
    if button.SetNormalAtlas then
        button:SetNormalAtlas("RedButton-Exit")
        button:SetPushedAtlas("RedButton-exit-pressed")
        button:SetDisabledAtlas("RedButton-Exit-Disabled")
        button:SetHighlightAtlas("RedButton-Highlight", "ADD")
    else
        local normal = CreateNativeAtlas(button, "ARTWORK", nil, "RedButton-Exit")
        button:SetNormalTexture(normal)
        local pushed = CreateNativeAtlas(button, "ARTWORK", nil, "RedButton-exit-pressed")
        button:SetPushedTexture(pushed)
        local disabled = CreateNativeAtlas(button, "ARTWORK", nil, "RedButton-Exit-Disabled")
        button:SetDisabledTexture(disabled)
        local highlight = CreateNativeAtlas(button, "HIGHLIGHT", nil, "RedButton-Highlight")
        highlight:SetBlendMode("ADD")
        button:SetHighlightTexture(highlight)
    end
    button:SetSize(24, 24)
    RefreshButton(button, state)
    return state
end

function BlizzardSkin:EditBox(editBox)
    if not editBox or not CanMutate(editBox) then return nil end
    local existing = styled[editBox]
    if existing then return existing end
    if editBox.SetBackdrop then editBox:SetBackdrop(nil) end
    if editBox.Left then editBox.Left:SetAlpha(0) end
    if editBox.Middle then editBox.Middle:SetAlpha(0) end
    if editBox.Right then editBox.Right:SetAlpha(0) end
    local state = { kind = "edit" }
    state.left = CreateNativeAtlas(editBox, "BACKGROUND", nil, ATLAS.searchLeft)
    state.left:SetSize(8, 20)
    state.left:SetPoint("LEFT", -5, 0)
    state.right = CreateNativeAtlas(editBox, "BACKGROUND", nil, ATLAS.searchRight)
    state.right:SetSize(8, 20)
    state.right:SetPoint("RIGHT", 0, 0)
    state.middle = CreateNativeAtlas(editBox, "BACKGROUND", nil, ATLAS.searchMiddle)
    state.middle:ClearAllPoints()
    state.middle:SetPoint("LEFT", state.left, "RIGHT")
    state.middle:SetPoint("RIGHT", state.right, "LEFT")
    state.middle:SetHeight(20)
    if _G.ChatFontNormal and editBox.SetFontObject then editBox:SetFontObject(_G.ChatFontNormal) end
    if editBox.SetTextColor then editBox:SetTextColor(0.95, 0.95, 0.95, 1) end
    styled[editBox] = state
    RefreshObject(editBox, state)
    editBox:HookScript("OnEditFocusGained", function(active)
        local activeState = styled[active]
        if activeState then activeState.focused = true; RefreshObject(active, activeState) end
    end)
    editBox:HookScript("OnEditFocusLost", function(active)
        local activeState = styled[active]
        if activeState then activeState.focused = false; RefreshObject(active, activeState) end
    end)
    return state
end

function BlizzardSkin:Text(fontString, role)
    if not fontString or not CanMutate(fontString) then return nil end
    local state = { kind = "text", role = role or "text" }
    styled[fontString] = state
    RefreshObject(fontString, state)
    return state
end

function BlizzardSkin:SetTextRole(fontString, role)
    local state = styled[fontString]
    if not state or state.kind ~= "text" then return end
    state.role = role or "text"
    RefreshObject(fontString, state)
end

function BlizzardSkin:SetFontRole(fontString, role)
    if not fontString then return end
    local state = styled[fontString] or self:Text(fontString)
    if not state then return end
    state.fontToken = role
    RefreshObject(fontString, state)
end

function BlizzardSkin:Accent(texture)
    if not texture or not CanMutate(texture) then return nil end
    SetColor(texture, COLORS.accent)
    return texture
end

function BlizzardSkin:InkRule(texture)
    if not texture or not CanMutate(texture) then return nil end
    texture:SetTexture(WHITE)
    SetColor(texture, COLORS.border)
    return texture
end

function BlizzardSkin:ScrollBar(slider, track, thumb)
    if not slider or not CanMutate(slider) then return nil end
    local state = { kind = "scrollbar", track = track, thumb = thumb }
    if thumb then
        state.glow = slider:CreateTexture(nil, "BACKGROUND", nil, 1)
        state.glow:SetPoint("TOPLEFT", thumb, "TOPLEFT", -2, 2)
        state.glow:SetPoint("BOTTOMRIGHT", thumb, "BOTTOMRIGHT", 2, -2)
    end
    styled[slider] = state
    RefreshObject(slider, state)
    return state
end

function BlizzardSkin:ThumbnailFrame(frame)
    if not frame or not CanMutate(frame) then return nil end
    if frame._cutsceneBlizzardThumbnailSkin then return frame._cutsceneBlizzardThumbnailSkin end
    if frame.SetBackdrop then frame:SetBackdrop(nil) end
    local state = { kind = "thumbnail" }
    state.fill = frame:CreateTexture(nil, "BACKGROUND", nil, -2)
    state.fill:SetAllPoints(frame)
    state.nineSlice = ApplyNineSlice(frame, "InsetFrameTemplate", "_cutsceneBlizzardThumbnailBorder")
    frame._cutsceneBlizzardThumbnailSkin = state
    styled[frame] = state
    RefreshObject(frame, state)
    return state
end

function BlizzardSkin:SetCardHovered(frame, hovered)
    local state = styled[frame]
    if not state or state.kind ~= "panel" then return end
    if state.hover then state.hover:SetShown(hovered == true) end
    if state.itemBackplate then state.itemBackplate:SetAlpha(hovered and 1 or 0.25) end
    TintNineSlice(state.nineSlice, hovered and COLORS.accent or COLORS.border)
end

function BlizzardSkin:FavoriteButton(button, selected)
    if not button or not CanMutate(button) then return nil end
    local state = button._cutsceneBlizzardFavorite
    if not state then
        state = {}
        state.shadow = CreateNativeAtlas(button, "BACKGROUND", nil, ATLAS.favorite)
        state.shadow:SetPoint("CENTER", 1, -1)
        state.shadow:SetVertexColor(0.08, 0.035, 0.015, 0.72)
        state.icon = CreateNativeAtlas(button, "ARTWORK", nil, ATLAS.favoriteOff)
        state.icon:SetPoint("CENTER")
        state.highlight = CreateNativeAtlas(button, "HIGHLIGHT", nil, ATLAS.favoriteOff)
        state.highlight:SetPoint("CENTER")
        state.highlight:SetBlendMode("ADD")
        state.highlight:SetAlpha(0.4)
        button._cutsceneBlizzardFavorite = state
    end
    SetAtlasSafe(state.icon, selected and ATLAS.favorite or ATLAS.favoriteOff, false)
    SetAtlasSafe(state.shadow, selected and ATLAS.favorite or ATLAS.favoriteOff, false)
    state.shadow:SetSize(22, 22)
    state.shadow:SetShown(selected == true)
    state.icon:SetSize(20, 20)
    if selected then
        state.icon:SetVertexColor(0.64, 0.36, 0.075, 1)
    else
        state.icon:SetVertexColor(0.34, 0.28, 0.20, 0.74)
    end
    state.highlight:SetSize(20, 20)
    state.highlight:SetAlpha(selected and 0.2 or 0.4)
    return state
end

function BlizzardSkin:FavoriteBookmark(card, imageFrame)
    if not card or not imageFrame or not CanMutate(card) then return nil end
    if card._memoriesFavoriteBookmark then return card._memoriesFavoriteBookmark end

    local marker = CreateFrame("Frame", nil, card)
    marker:SetSize(18, 43)
    marker:SetPoint("TOPLEFT", imageFrame, "TOPLEFT", 11, 4)
    marker:SetFrameLevel(card:GetFrameLevel() + 7)
    marker:EnableMouse(false)

    local function Cloth(layer, sublevel, left, top, width, height, color)
        local texture = marker:CreateTexture(nil, layer, nil, sublevel)
        texture:SetTexture(WHITE)
        texture:SetPoint("TOPLEFT", marker, "TOPLEFT", left, top)
        texture:SetSize(width, height)
        texture:SetVertexColor(color[1], color[2], color[3], color[4] or 1)
        return texture
    end

    local function Ribbon(layer, sublevel, left, top, width, height, color)
        local texture = marker:CreateTexture(nil, layer, nil, sublevel)
        texture:SetTexture(FAVORITE_BOOKMARK_SHAPE)
        texture:SetPoint("TOPLEFT", marker, "TOPLEFT", left, top)
        texture:SetSize(width, height)
        texture:SetVertexColor(color[1], color[2], color[3], color[4] or 1)
        return texture
    end

    Ribbon("BACKGROUND", 0, 2, -2, 16, 43, { 0.08, 0.035, 0.012, 0.66 })
    Ribbon("ARTWORK", 0, 0, 0, 16, 41, { 0.48, 0.245, 0.035, 1 })
    Ribbon("ARTWORK", 1, 2, -1, 11, 38, { 0.84, 0.52, 0.09, 1 })
    Cloth("ARTWORK", 2, 3, -2, 2, 29, { 1.0, 0.76, 0.24, 0.72 })
    Cloth("ARTWORK", 2, 12, -2, 2, 29, { 0.25, 0.105, 0.018, 0.48 })
    Cloth("OVERLAY", 0, 1, 0, 14, 1, { 1.0, 0.79, 0.30, 0.92 })
    Cloth("OVERLAY", 0, 2, -10, 12, 1, { 0.26, 0.11, 0.018, 0.24 })
    Cloth("OVERLAY", 0, 2, -20, 12, 1, { 1.0, 0.78, 0.30, 0.14 })

    marker:Hide()
    card._memoriesFavoriteBookmark = marker
    return marker
end
