local ADDON_NAME, RPAL = ...
local HS=RPAL.Hearthstone
if not HS then return end

local FONT=STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF"
local TITLE_FONT="Fonts\\MORPHEUS.TTF"
local DISPLAY=FONT
local WHITE="Interface\\Buttons\\WHITE8X8"
local TB="Interface\\AddOns\\ODYN\\Textures\\TravelBookLite\\"
local HS_WOOD="Interface\\AddOns\\ODYN\\Textures\\Hearthstone\\hs_wood.tga"
local HS_LEATHER="Interface\\AddOns\\ODYN\\Textures\\Hearthstone\\hs_leather.tga"
local HS_PARCHMENT="Interface\\AddOns\\ODYN\\Textures\\Hearthstone\\hs_parchment.tga"
local DETAIL_FRAME="Interface\\AddOns\\ODYN\\Textures\\PremadeUI\\detail_frame.tga"
local CELESTIAL="Interface\\AddOns\\ODYN\\Textures\\Celestial\\"
local ORNAMENT_ROYAL="Interface\\AddOns\\ODYN\\Textures\\Cosmetics\\ornament_royal_corner.tga"
local ORNAMENT_SEAL="Interface\\AddOns\\ODYN\\Textures\\Cosmetics\\ornament_royal_seal.tga"
local ORNAMENT_ROOT="Interface\\AddOns\\ODYN\\Textures\\Cosmetics\\ornament_"
local ATMOSPHERE_ROOT="Interface\\AddOns\\ODYN\\Textures\\Atmospheres\\"
local BOARD_SPACING=154
local STAT_ATTACK="Interface\\Icons\\INV_Sword_04"
local STAT_HEALTH=TB.."UI_TravelBook_IconHeart01a.tga"
local frame,matchFrame,challengeFrame,packFrame,statusToast,cardInspect
local selectedTab="play"
local selectedDeckID
local pendingTarget
local collectionSearch=""
local collectionTypeFilter="All"
local collectionRarityFilter="All"
local collectionManaFilter="All"
local collectionSortKey="Mana"
local collectionSortAsc=true
local collectionPage=1
local collectionScrollY=0
local cosmeticArenaPage=1
local cosmeticBackPage=1

-- Hearthstone intentionally keeps its own warm tavern palette.  It does not
-- inherit the player's RPAL profile theme: entering the card table should feel
-- like stepping into the same quiet wooden tavern regardless of character.
local TAVERN={
    gold={.96,.73,.35,1}, pale={.97,.89,.76,1}, muted={.74,.63,.50,1},
    main={.22,.125,.068,.995}, panel={.29,.165,.088,.985}, panel2={.235,.132,.070,.985},
    border={.50,.315,.165,1}, accent={.84,.53,.24,1}, accentHover={1,.76,.35,1},
    button={.24,.13,.070,1}, cyan={.45,.84,.86,1}, ink={.18,.105,.055,1}, parchment={.88,.75,.54,1},
}
local function P() return TAVERN end
local function Scaled(n)
    local v=RPAL.GetScaledFontSize and RPAL.GetScaledFontSize(n) or n
    return v*1.08
end
local function Backdrop(f,border,alpha)
    local p=P()
    f:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=11,insets={left=2,right=2,top=2,bottom=2}})
    f:SetBackdropColor(.090,.044,.025,alpha or .98)
    f:SetBackdropBorderColor((border or p.border)[1],(border or p.border)[2],(border or p.border)[3],.94)
end
local function AddWood(parent,alpha)
    local t=parent:CreateTexture(nil,"BACKGROUND",nil,-8);t:SetTexture(HS_WOOD);t:SetAllPoints();if t.SetHorizTile then t:SetHorizTile(true);t:SetVertTile(true) end;t:SetAlpha(alpha or .94);return t
end
local function AddParchment(parent,alpha)
    local t=parent:CreateTexture(nil,"BACKGROUND",nil,-5);t:SetTexture(HS_PARCHMENT);t:SetAllPoints();if t.SetHorizTile then t:SetHorizTile(true);t:SetVertTile(true) end;t:SetVertexColor(.72,.56,.34,1);t:SetAlpha(alpha or .14);return t
end
local function Text(parent,text,size,color,font)
    local t=parent:CreateFontString(nil,"OVERLAY")
    size=math.max(7.8,tonumber(size) or 10)
    t:SetFont(font or FONT,Scaled(size),"OUTLINE")
    local c=color or P().pale;t:SetTextColor(c[1],c[2],c[3],c[4] or 1);t:SetText(text or "")
    t:SetShadowColor(0,0,0,.72);t:SetShadowOffset(1,-1)
    return t
end
local function CardText(parent,text,size,color,font)
    local t=parent:CreateFontString(nil,"OVERLAY")
    size=math.max(7.0,tonumber(size) or 10)
    t:SetFont(font or FONT,Scaled(size),"")
    local c=color or P().pale;t:SetTextColor(c[1],c[2],c[3],c[4] or 1);t:SetText(text or "")
    t:SetShadowColor(0,0,0,.55);t:SetShadowOffset(1,-1)
    return t
end

local function Btn(parent,label,w,h,accent)
    local b=CreateFrame("Button",nil,parent,"BackdropTemplate");b:SetSize(w or 110,h or 30);Backdrop(b,accent and P().gold or P().border,.99);b:SetBackdropColor(accent and .30 or .23,accent and .16 or .115,accent and .065 or .055,.99);b.label=Text(b,label,9.5,accent and {1,.82,.47,1} or P().pale);b.label:SetPoint("CENTER");
    local shine=b:CreateTexture(nil,"BORDER");shine:SetTexture(WHITE);shine:SetColorTexture(1,.78,.35,.055);shine:SetPoint("TOPLEFT",3,-3);shine:SetPoint("TOPRIGHT",-3,-3);shine:SetHeight(math.max(1,(h or 30)*.28));
    b:SetScript("OnEnter",function(self)local p=P();self:SetBackdropBorderColor(p.gold[1],p.gold[2],p.gold[3],1);self:SetBackdropColor(.34,.19,.08,1) end);b:SetScript("OnLeave",function(self)local p=P();local c=accent and p.gold or p.border;self:SetBackdropBorderColor(c[1],c[2],c[3],.9);self:SetBackdropColor(accent and .30 or .23,accent and .16 or .115,accent and .065 or .055,.99) end);if RPAL.AttachFocusMark then RPAL.AttachFocusMark(b,P().gold) end;return b
end
local function Clear(parent)
    if not parent then return end
    local children={parent:GetChildren()};for _,c in ipairs(children) do if c._hsDynamic then c:Hide();c:SetParent(nil) end end
    local regions={parent:GetRegions()};for _,r in ipairs(regions) do if r._hsDynamic then r:Hide() end end
end
local function Dynamic(f) f._hsDynamic=true;return f end
local function DText(parent,... ) local t=Text(parent,...);t._hsDynamic=true;return t end
local function DBtn(parent,... ) local b=Btn(parent,...);b._hsDynamic=true;return b end
local function DressMenuPanel(panel,accent,sealAlpha)
    if not panel or panel._hsDressed then return panel end
    panel._hsDressed=true
    accent=accent or P().gold
    local wash=panel:CreateTexture(nil,"BACKGROUND",nil,1);wash:SetTexture(WHITE);wash:SetPoint("TOPLEFT",3,-3);wash:SetPoint("TOPRIGHT",-3,-3);wash:SetHeight(58)
    if wash.SetGradientAlpha then wash:SetGradientAlpha("VERTICAL",math.min(1,accent[1]*.34),math.min(1,accent[2]*.28),math.min(1,accent[3]*.22),.34,.06,.025,.015,.01) else wash:SetColorTexture(accent[1]*.22,accent[2]*.16,accent[3]*.12,.18) end
    local top=panel:CreateTexture(nil,"BORDER");top:SetTexture(WHITE);top:SetPoint("TOPLEFT",9,-7);top:SetPoint("TOPRIGHT",-9,-7);top:SetHeight(2);top:SetColorTexture(accent[1],accent[2],accent[3],.56)
    local bottom=panel:CreateTexture(nil,"BORDER");bottom:SetTexture(WHITE);bottom:SetPoint("BOTTOMLEFT",12,7);bottom:SetPoint("BOTTOMRIGHT",-12,7);bottom:SetHeight(1);bottom:SetColorTexture(accent[1],accent[2],accent[3],.18)
    local tl=panel:CreateTexture(nil,"BORDER");tl:SetTexture(ORNAMENT_ROYAL);tl:SetSize(58,58);tl:SetPoint("TOPLEFT",2,-2);tl:SetVertexColor(accent[1],accent[2],accent[3],.20)
    local br=panel:CreateTexture(nil,"BORDER");br:SetTexture(ORNAMENT_ROYAL);br:SetSize(58,58);br:SetPoint("BOTTOMRIGHT",-2,2);br:SetVertexColor(accent[1],accent[2],accent[3],.18);if br.SetRotation then br:SetRotation(3.1415926) end
    return panel
end

local function CardColor(card) return HS.GetCardRarityColor(card and card.rarity) end
local function CardLabel(card) return string.format("[%d] %s",card.cost or 0,card.name or "Card") end
local function Short(s,n) s=tostring(s or "");return #s>n and (s:sub(1,n-3).."...") or s end

local collectionDragGhost
local function MouseOverFrame(frame)
    if not frame or not frame.IsShown or not frame:IsShown() then return false end
    if MouseIsOver then local ok,v=pcall(MouseIsOver,frame);if ok then return v and true or false end end
    if frame.IsMouseOver then local ok,v=pcall(frame.IsMouseOver,frame);if ok then return v and true or false end end
    return false
end
local function StopCollectionDragGhost()
    if collectionDragGhost then collectionDragGhost:Hide();collectionDragGhost:SetScript("OnUpdate",nil) end
end
local function StartCollectionDragGhost(card,mode)
    StopCollectionDragGhost()
    if not collectionDragGhost then
        local g=CreateFrame("Frame",nil,UIParent,"BackdropTemplate");collectionDragGhost=g;g:SetSize(188,44);g:SetFrameStrata("TOOLTIP");g:SetFrameLevel(9900);g:EnableMouse(false)
        g:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=8,insets={left=2,right=2,top=2,bottom=2}})
        g.icon=g:CreateTexture(nil,"ARTWORK");g.icon:SetSize(34,34);g.icon:SetPoint("LEFT",5,0);g.icon:SetTexCoord(.07,.93,.07,.93)
        g.text=CardText(g,"",8.8,P().pale,FONT);g.text:SetPoint("LEFT",g.icon,"RIGHT",7,0);g.text:SetPoint("RIGHT",-7,0);g.text:SetJustifyH("LEFT");g.text:SetWordWrap(false)
        g.mode=CardText(g,"",6.5,P().gold,FONT);g.mode:SetPoint("BOTTOMRIGHT",-7,3)
    end
    local rc=CardColor(card);collectionDragGhost:SetBackdropColor(.08,.035,.018,.97);collectionDragGhost:SetBackdropBorderColor(rc[1],rc[2],rc[3],1)
    collectionDragGhost.icon:SetTexture(card and card.icon or "Interface\\Icons\\INV_Misc_QuestionMark")
    collectionDragGhost.text:SetText(Short(card and card.name or "Card",23));collectionDragGhost.mode:SetText(mode=="remove" and "DROP OUTSIDE DECK TO REMOVE" or "DROP ON DECK TO ADD")
    collectionDragGhost:Show();collectionDragGhost:SetScript("OnUpdate",function(self)
        local x,y=GetCursorPosition();local scale=(UIParent and UIParent.GetEffectiveScale and UIParent:GetEffectiveScale()) or 1
        self:ClearAllPoints();self:SetPoint("BOTTOMLEFT",UIParent,"BOTTOMLEFT",x/scale+14,y/scale+12)
    end)
end

local function ShowStatus(msg)
    if not statusToast then
        statusToast=CreateFrame("Frame",nil,UIParent,"BackdropTemplate");statusToast:SetSize(430,44);statusToast:SetPoint("TOP",0,-130);statusToast:SetFrameStrata("TOOLTIP");Backdrop(statusToast,P().gold,.98);statusToast.text=Text(statusToast,"",10,P().pale);statusToast.text:SetPoint("CENTER");statusToast:Hide()
    end
    if RPAL.ApplyToastLayout then RPAL.ApplyToastLayout(statusToast) end
    statusToast._serial=(statusToast._serial or 0)+1;local serial=statusToast._serial
    statusToast.text:SetText(tostring(msg or ""));statusToast:SetAlpha(1);statusToast:Show();if C_Timer and C_Timer.After then C_Timer.After(2.7,function()if statusToast and statusToast._serial==serial then statusToast:Hide() end end) end
end
RPAL.HearthstoneStatus=ShowStatus

local ShowCardInspect, HideCardInspect
local function CardBodyColor() return {.17,.10,.045,1} end
local function CardSubColor() return {.38,.255,.13,1} end

-- Hearthstone card presentation is intentionally 2D and uses card artwork only.
local function MakeStatBadge(parent,value,kind,w,h)
    local badge=CreateFrame("Frame",nil,parent)
    local size=math.max(28,math.floor((w or 180)*.17))
    badge:SetSize(size,size)
    badge.glow=badge:CreateTexture(nil,"BACKGROUND")
    badge.glow:SetTexture(CELESTIAL.."starlight_glow.tga")
    badge.glow:SetPoint("CENTER");badge.glow:SetSize(size*1.42,size*1.42)
    badge.glow:SetVertexColor(1,kind=="health" and .16 or .54,kind=="health" and .10 or .08,.18)
    badge.icon=badge:CreateTexture(nil,"ARTWORK")
    badge.icon:SetAllPoints();badge.icon:SetTexture(kind=="health" and STAT_HEALTH or STAT_ATTACK)
    if kind=="health" then
        badge.icon:SetVertexColor(1,.24,.20,.96)
    else
        badge.icon:SetTexCoord(.16,.84,.16,.84);badge.icon:SetVertexColor(.96,.72,.28,.96)
    end
    local numberSize=math.max(16,math.floor((w or 180)*.090))
    local t=Text(badge,tostring(value or 0),numberSize,{1,1,1,1},FONT)
    t:SetPoint("CENTER",0,0);t:SetShadowColor(0,0,0,1);t:SetShadowOffset(1,-1)
    badge.value=t
    return badge
end

local function ArenaDef(id)
    return (HS.GetArenaDefinition and HS.GetArenaDefinition(id)) or {id="tavern",name="Tavern Hearth",rail={.24,.095,.085},field={.74,.53,.31},accent={.96,.66,.27},ornament="royal"}
end
local function CardBackDef(id)
    return (HS.GetCardBackDefinition and HS.GetCardBackDefinition(id)) or {id="classic",name="Classic Sigil",base={.16,.32,.62},edge={.94,.68,.25},glow={.22,.70,1}}
end
local function OrnamentForArena(def,seal)
    local key=(def and def.ornament) or "royal"
    return ORNAMENT_ROOT..key..(seal and "_seal.tga" or "_corner.tga")
end
local ARENA_PATTERN={
    tavern=ATMOSPHERE_ROOT.."atmosphere_candlelit.tga",
    crimson=ATMOSPHERE_ROOT.."atmosphere_ember.tga",
    arcane=ATMOSPHERE_ROOT.."atmosphere_arcane.tga",
    frost=ATMOSPHERE_ROOT.."atmosphere_frost.tga",
    verdant=ATMOSPHERE_ROOT.."atmosphere_dream.tga",
    storm=ATMOSPHERE_ROOT.."atmosphere_stormdawn.tga",
    moon=ATMOSPHERE_ROOT.."atmosphere_moonmist.tga",
    zodiac=ATMOSPHERE_ROOT.."atmosphere_zodiac_veil.tga",
}
local function ArenaPattern(def)
    return ARENA_PATTERN[(def and def.id) or "tavern"] or HS_PARCHMENT
end
local function ArenaBanner(def)
    local key=(def and def.ornament) or "royal"
    return "Interface\\AddOns\\ODYN\\Textures\\Cosmetics\\header_"..key.."_banner.tga"
end
local CARD_BACK_PATTERN={
    classic=ATMOSPHERE_ROOT.."atmosphere_celestial_chart.tga",
    ember=ATMOSPHERE_ROOT.."atmosphere_ember.tga",
    arcane=ATMOSPHERE_ROOT.."atmosphere_arcane.tga",
    frost=ATMOSPHERE_ROOT.."atmosphere_frost.tga",
    royal=ATMOSPHERE_ROOT.."atmosphere_candlelit.tga",
    bloodmoon=ATMOSPHERE_ROOT.."atmosphere_ember.tga",
    verdant=ATMOSPHERE_ROOT.."atmosphere_dream.tga",
    voidglass=ATMOSPHERE_ROOT.."atmosphere_voidveil.tga",
    sunforge=ATMOSPHERE_ROOT.."atmosphere_astral_rays.tga",
    stormsteel=ATMOSPHERE_ROOT.."atmosphere_stormdawn.tga",
    gravebound=ATMOSPHERE_ROOT.."atmosphere_gravefog.tga",
    rose=ATMOSPHERE_ROOT.."atmosphere_starfall_veil.tga",
    nightveil=ATMOSPHERE_ROOT.."atmosphere_moonmist.tga",
    runic=ATMOSPHERE_ROOT.."atmosphere_zodiac_veil.tga",
    tidesage=ATMOSPHERE_ROOT.."atmosphere_frost.tga",
    sandgold=ATMOSPHERE_ROOT.."atmosphere_candlelit.tga",
    starfall=ATMOSPHERE_ROOT.."atmosphere_celestial_chart.tga",
    dreamweave=ATMOSPHERE_ROOT.."atmosphere_dream.tga",
}
local CARD_BACK_ORNAMENT={
    classic="royal",ember="ember",arcane="arcane",frost="frost",royal="royal",bloodmoon="ember",verdant="kaldorei",voidglass="void",sunforge="royal",stormsteel="runic",gravebound="runic",rose="kaldorei",nightveil="kaldorei",runic="runic",tidesage="frost",sandgold="royal",starfall="arcane",dreamweave="kaldorei"
}
local CARD_BACK_STYLE={
    classic="seal",ember="bands",arcane="diamond",frost="halo",royal="crown",bloodmoon="split",verdant="vine",voidglass="void",sunforge="sun",stormsteel="storm",gravebound="grave",rose="rose",nightveil="moon",runic="runes",tidesage="tide",sandgold="sun",starfall="stars",dreamweave="vine"
}
local function MakeCardBack(parent,id,w,h,flipped)
    local def=CardBackDef(id);w=w or 74;h=h or 106
    local ornament=CARD_BACK_ORNAMENT[def.id] or "royal"
    local style=CARD_BACK_STYLE[def.id] or "seal"
    local b=CreateFrame("Frame",nil,parent,"BackdropTemplate");b:SetSize(w,h);b:EnableMouse(false)
    b:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=10,insets={left=3,right=3,top=3,bottom=3}})
    b:SetBackdropColor(def.base[1]*.28,def.base[2]*.28,def.base[3]*.28,.998);b:SetBackdropBorderColor(def.edge[1],def.edge[2],def.edge[3],1)

    local body=b:CreateTexture(nil,"BACKGROUND");body:SetTexture(WHITE);body:SetPoint("TOPLEFT",5,-5);body:SetPoint("BOTTOMRIGHT",-5,5)
    if body.SetGradientAlpha then body:SetGradientAlpha("VERTICAL",math.min(1,def.base[1]*1.55),math.min(1,def.base[2]*1.55),math.min(1,def.base[3]*1.55),1,def.base[1]*.28,def.base[2]*.28,def.base[3]*.28,1) else body:SetColorTexture(def.base[1],def.base[2],def.base[3],1) end
    local pattern=b:CreateTexture(nil,"BACKGROUND",nil,1);pattern:SetTexture(CARD_BACK_PATTERN[def.id] or CARD_BACK_PATTERN.classic);pattern:SetPoint("TOPLEFT",7,-7);pattern:SetPoint("BOTTOMRIGHT",-7,7);pattern:SetVertexColor(def.glow[1],def.glow[2],def.glow[3],.16)
    if flipped and pattern.SetRotation then pattern:SetRotation(3.1415926) end
    local vignette=b:CreateTexture(nil,"BORDER",nil,1);vignette:SetTexture(WHITE);vignette:SetPoint("TOPLEFT",7,-7);vignette:SetPoint("BOTTOMRIGHT",-7,7);vignette:SetColorTexture(.015,.008,.018,.30)

    -- Triple inset frame makes the back read as a crafted physical card rather
    -- than a recolored rectangle.
    for i=1,3 do
        local inset=CreateFrame("Frame",nil,b,"BackdropTemplate");local pad=8+i*4;inset:SetPoint("TOPLEFT",pad,-pad);inset:SetPoint("BOTTOMRIGHT",-pad,pad);inset:SetBackdrop({edgeFile=(i==3 and WHITE or "Interface\\Tooltips\\UI-Tooltip-Border"),edgeSize=(i==3 and 1 or 6)});inset:SetBackdropBorderColor(def.edge[1],def.edge[2],def.edge[3],i==1 and .72 or (i==2 and .40 or .24));inset:EnableMouse(false)
    end

    local spine=b:CreateTexture(nil,"ARTWORK",nil,1);spine:SetTexture(WHITE);spine:SetSize(math.max(2,w*.035),h*.64);spine:SetPoint("CENTER");spine:SetColorTexture(def.edge[1],def.edge[2],def.edge[3],.18)
    local cross=b:CreateTexture(nil,"ARTWORK",nil,1);cross:SetTexture(WHITE);cross:SetSize(w*.64,math.max(2,h*.025));cross:SetPoint("CENTER");cross:SetColorTexture(def.edge[1],def.edge[2],def.edge[3],.15)

    local medallion=b:CreateTexture(nil,"ARTWORK",nil,4);medallion:SetTexture(ORNAMENT_ROOT..ornament.."_seal.tga");medallion:SetSize(w*.64,w*.64);medallion:SetPoint("CENTER");medallion:SetVertexColor(def.edge[1],def.edge[2],def.edge[3],.88)
    local core=b:CreateTexture(nil,"OVERLAY",nil,5);core:SetTexture(CELESTIAL.."starlight_diamond.tga");core:SetBlendMode("ADD");core:SetSize(w*.31,w*.31);core:SetPoint("CENTER");core:SetVertexColor(def.glow[1],def.glow[2],def.glow[3],.72)
    local halo=b:CreateTexture(nil,"ARTWORK",nil,3);halo:SetTexture(CELESTIAL.."starlight_round.tga");halo:SetBlendMode("ADD");halo:SetSize(w*.92,w*.92);halo:SetPoint("CENTER");halo:SetVertexColor(def.glow[1],def.glow[2],def.glow[3],.20)

    local function Corner(point,x,y,rotation,alpha)
        local c=b:CreateTexture(nil,"OVERLAY",nil,6);c:SetTexture(ORNAMENT_ROOT..ornament.."_corner.tga");c:SetSize(w*.34,w*.34);c:SetPoint(point,x,y);c:SetVertexColor(def.edge[1],def.edge[2],def.edge[3],alpha or .58);if c.SetRotation and rotation then c:SetRotation(rotation) end;return c
    end
    -- ornament art is authored bottom-left: rotate each corner into its native direction
    Corner("BOTTOMLEFT",2,2,0,.60);Corner("BOTTOMRIGHT",-2,2,1.5707963,.56);Corner("TOPRIGHT",-2,-2,3.1415926,.60);Corner("TOPLEFT",2,-2,-1.5707963,.56)

    local topCap=b:CreateTexture(nil,"OVERLAY",nil,5);topCap:SetTexture(ORNAMENT_ROOT..ornament.."_corner.tga");topCap:SetSize(w*.46,w*.24);topCap:SetPoint("TOP",0,-5);topCap:SetVertexColor(def.edge[1],def.edge[2],def.edge[3],.34);if topCap.SetRotation then topCap:SetRotation(-.785398) end
    local bottomCap=b:CreateTexture(nil,"OVERLAY",nil,5);bottomCap:SetTexture(ORNAMENT_ROOT..ornament.."_corner.tga");bottomCap:SetSize(w*.46,w*.24);bottomCap:SetPoint("BOTTOM",0,5);bottomCap:SetVertexColor(def.edge[1],def.edge[2],def.edge[3],.30);if bottomCap.SetRotation then bottomCap:SetRotation(2.356194) end

    -- Each unlock now has a visibly different motif, not just a different hue.
    if style=="bands" then
        medallion:SetSize(w*.50,w*.50);for _,yy in ipairs({-.25,0,.25}) do local band=b:CreateTexture(nil,"ARTWORK",nil,2);band:SetTexture(WHITE);band:SetSize(w*.72,math.max(2,h*.035));band:SetPoint("CENTER",0,h*yy);band:SetColorTexture(def.edge[1],def.edge[2],def.edge[3],yy==0 and .48 or .24) end
    elseif style=="diamond" or style=="void" then
        medallion:SetAlpha(.50);local d=b:CreateTexture(nil,"ARTWORK",nil,3);d:SetTexture(CELESTIAL.."starlight_diamond.tga");d:SetBlendMode("ADD");d:SetSize(w*.96,w*.96);d:SetPoint("CENTER");d:SetVertexColor(def.glow[1],def.glow[2],def.glow[3],style=="void" and .56 or .38);if d.SetRotation then d:SetRotation(.785398) end
    elseif style=="halo" or style=="moon" then
        halo:SetAlpha(.68);halo:SetSize(w*1.02,w*1.02);if style=="moon" then medallion:ClearAllPoints();medallion:SetPoint("CENTER",0,h*.08);core:ClearAllPoints();core:SetPoint("CENTER",0,h*.08);local cres=b:CreateTexture(nil,"OVERLAY",nil,4);cres:SetTexture(CELESTIAL.."starlight_round.tga");cres:SetBlendMode("ADD");cres:SetSize(w*.46,w*.46);cres:SetPoint("CENTER",w*.10,-h*.01);cres:SetVertexColor(.02,.02,.05,.92) end
    elseif style=="split" or style=="rose" then
        spine:SetWidth(math.max(4,w*.075));spine:SetColorTexture(def.edge[1],def.edge[2],def.edge[3],.44);medallion:SetSize(w*.48,w*.48);local side=b:CreateTexture(nil,"ARTWORK",nil,2);side:SetTexture(WHITE);side:SetPoint("TOPLEFT",12,-15);side:SetPoint("BOTTOMLEFT",12,15);side:SetWidth(w*.18);side:SetColorTexture(def.glow[1],def.glow[2],def.glow[3],.12)
    elseif style=="runes" or style=="grave" or style=="storm" then
        medallion:SetSize(w*.46,w*.46);for i=1,4 do local r=b:CreateTexture(nil,"ARTWORK",nil,3);r:SetTexture(ORNAMENT_ROOT..ornament.."_seal.tga");r:SetSize(w*.18,w*.18);local sx=(i%2==1) and -w*.23 or w*.23;local sy=(i<=2) and h*.23 or -h*.23;r:SetPoint("CENTER",sx,sy);r:SetVertexColor(def.glow[1],def.glow[2],def.glow[3],style=="grave" and .30 or .44) end
    elseif style=="sun" or style=="crown" then
        local rays=b:CreateTexture(nil,"ARTWORK",nil,2);rays:SetTexture(CELESTIAL.."starlight_glow.tga");rays:SetBlendMode("ADD");rays:SetSize(w*1.18,w*1.18);rays:SetPoint("CENTER");rays:SetVertexColor(def.glow[1],def.glow[2],def.glow[3],.44);medallion:SetSize(w*.56,w*.56)
    elseif style=="stars" then
        medallion:SetAlpha(.38);core:SetAlpha(.92);local pts={{-.24,.22},{.25,.25},{-.28,-.20},{.28,-.23},{0,.34}}
        for i,pt in ipairs(pts) do local d=b:CreateTexture(nil,"OVERLAY",nil,4);d:SetTexture(CELESTIAL.."starlight_diamond.tga");d:SetBlendMode("ADD");d:SetSize(w*(i==5 and .14 or .09),w*(i==5 and .14 or .09));d:SetPoint("CENTER",w*pt[1],h*pt[2]);d:SetVertexColor(def.glow[1],def.glow[2],def.glow[3],i==5 and .82 or .52) end
    elseif style=="vine" then
        medallion:SetSize(w*.50,w*.50);local v1=b:CreateTexture(nil,"ARTWORK",nil,3);v1:SetTexture(ORNAMENT_ROOT..ornament.."_corner.tga");v1:SetSize(w*.62,w*.62);v1:SetPoint("LEFT",4,0);v1:SetVertexColor(def.glow[1],def.glow[2],def.glow[3],.40);local v2=b:CreateTexture(nil,"ARTWORK",nil,3);v2:SetTexture(ORNAMENT_ROOT..ornament.."_corner.tga");v2:SetSize(w*.62,w*.62);v2:SetPoint("RIGHT",-4,0);v2:SetVertexColor(def.glow[1],def.glow[2],def.glow[3],.36);if v2.SetRotation then v2:SetRotation(3.1415926) end
    elseif style=="tide" then
        medallion:SetSize(w*.48,w*.48);for i=1,3 do local wave=b:CreateTexture(nil,"ARTWORK",nil,2);wave:SetTexture(CELESTIAL.."starlight_round.tga");wave:SetBlendMode("ADD");wave:SetSize(w*(.55+i*.11),w*(.22+i*.04));wave:SetPoint("CENTER",0,-h*.18+i*5);wave:SetVertexColor(def.glow[1],def.glow[2],def.glow[3],.13+i*.07) end
    end
    if flipped then if medallion.SetRotation then medallion:SetRotation(3.1415926) end;if core.SetRotation then core:SetRotation(3.1415926) end end
    b._backDef=def;return b
end

local function AddCardCornerChrome(parent,card,alpha)
    local rc=CardColor(card)
    local corner=parent:CreateTexture(nil,"OVERLAY")
    corner:SetTexture(DETAIL_FRAME);corner:SetAllPoints();corner:SetVertexColor(.96,.78,.45,alpha or .42)
    local rarityWash=parent:CreateTexture(nil,"BORDER")
    rarityWash:SetTexture(WHITE);rarityWash:SetPoint("TOPLEFT",6,-5);rarityWash:SetPoint("TOPRIGHT",-6,-5);rarityWash:SetHeight(3)
    rarityWash:SetColorTexture(rc[1],rc[2],rc[3],.78)
    local rarityFoot=parent:CreateTexture(nil,"BORDER")
    rarityFoot:SetTexture(WHITE);rarityFoot:SetPoint("BOTTOMLEFT",10,6);rarityFoot:SetPoint("BOTTOMRIGHT",-10,6);rarityFoot:SetHeight(2);rarityFoot:SetColorTexture(rc[1],rc[2],rc[3],.40)
    return corner
end

local function AddOuterHoverRing(parent,card,pad)
    pad=pad or 5
    local rc=CardColor(card)
    local ring=CreateFrame("Frame",nil,parent,"BackdropTemplate")
    ring:SetPoint("TOPLEFT",-pad,pad);ring:SetPoint("BOTTOMRIGHT",pad,-pad);ring:EnableMouse(false)
    ring:SetFrameLevel((parent:GetFrameLevel() or 1)+18)
    ring:SetBackdrop({edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=13,insets={left=1,right=1,top=1,bottom=1}})
    ring:SetBackdropBorderColor(rc[1],rc[2],rc[3],1)
    local glow=ring:CreateTexture(nil,"BACKGROUND")
    glow:SetTexture(CELESTIAL.."starlight_glow.tga");glow:SetBlendMode("ADD");glow:SetPoint("TOPLEFT",-18,18);glow:SetPoint("BOTTOMRIGHT",18,-18);glow:SetVertexColor(rc[1],rc[2],rc[3],.60)
    ring.glow=glow;ring:SetAlpha(0)
    return ring
end

local function StartCardParallax(frame,maxX,maxY)
    if not frame or frame._cardParallax then return end
    local point,relative,relativePoint,x,y=frame:GetPoint(1);if not point then return end
    frame._cardParallax={point=point,relative=relative,relativePoint=relativePoint,x=tonumber(x) or 0,y=tonumber(y) or 0}
    if not frame._cardParallaxDriver then frame._cardParallaxDriver=CreateFrame("Frame",nil,frame) end
    frame._cardParallaxDriver:Show();frame._cardParallaxDriver:SetScript("OnUpdate",function()
        if not frame._cardParallax or not frame:IsShown() then return end
        local l,r,b,t=frame:GetLeft(),frame:GetRight(),frame:GetBottom(),frame:GetTop();if not l or not r or not b or not t then return end
        local cx,cy=(l+r)*.5,(b+t)*.5;local px,py=GetCursorPosition();local scale=UIParent:GetEffectiveScale() or 1;px=px/scale;py=py/scale
        local dx=math.max(-1,math.min(1,(px-cx)/math.max(1,(r-l)*.5)));local dy=math.max(-1,math.min(1,(py-cy)/math.max(1,(t-b)*.5)));local a=frame._cardParallax
        frame:ClearAllPoints();frame:SetPoint(a.point,a.relative,a.relativePoint,a.x+dx*(maxX or 3.2),a.y+dy*(maxY or 2.2))
    end)
end
local function StopCardParallax(frame)
    if not frame or not frame._cardParallax then return end
    local a=frame._cardParallax;frame._cardParallax=nil
    if frame._cardParallaxDriver then frame._cardParallaxDriver:SetScript("OnUpdate",nil);frame._cardParallaxDriver:Hide() end
    frame:ClearAllPoints();frame:SetPoint(a.point,a.relative,a.relativePoint,a.x,a.y)
end

local function BaseGlowAlpha(card)
    return card.rarity=="Legendary" and .72 or (card.rarity=="Epic" and .44 or (card.rarity=="Rare" and .25 or .08))
end

local function MakeManaGem(parent,cost,size)
    size=size or 34
    local gem=CreateFrame("Frame",nil,parent,"BackdropTemplate");gem:SetSize(size,size)
    gem:SetBackdrop({bgFile="Interface\\Icons\\INV_Misc_Gem_Sapphire_01",edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=8,insets={left=2,right=2,top=2,bottom=2}})
    gem:SetBackdropColor(.25,.48,1,1);gem:SetBackdropBorderColor(.40,.76,1,1)
    local n=Text(gem,tostring(cost or 0),math.max(16,size*.46),{1,1,1,1},FONT)
    n:SetPoint("CENTER",0,0);n:SetShadowColor(0,.02,.12,1);n:SetShadowOffset(1,-1)
    gem.value=n
    return gem
end

local function MakeCompactCard(parent,card,w,h)
    w=w or 96;h=h or 150
    local rc=CardColor(card)
    local c=Dynamic(CreateFrame("Button",nil,parent,"BackdropTemplate"));c:SetSize(w,h);c._baseLevel=c:GetFrameLevel();c._hsCard=card
    c:SetBackdrop({bgFile=HS_LEATHER,tile=true,tileSize=64,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=9,insets={left=3,right=3,top=3,bottom=3}})
    c:SetBackdropColor(.255,.145,.073,.995);c:SetBackdropBorderColor(rc[1],rc[2],rc[3],.94)
    local shadow=c:CreateTexture(nil,"BACKGROUND");shadow:SetTexture(WHITE);shadow:SetPoint("TOPLEFT",7,-8);shadow:SetPoint("BOTTOMRIGHT",-7,7);shadow:SetColorTexture(.03,.012,.004,.28)
    local rarityWash=c:CreateTexture(nil,"BACKGROUND",nil,-1);rarityWash:SetTexture(CELESTIAL.."starlight_round.tga");rarityWash:SetBlendMode("ADD");rarityWash:SetSize(w*1.15,h*.62);rarityWash:SetPoint("TOP",0,10);rarityWash:SetVertexColor(rc[1],rc[2],rc[3],card.rarity=="Legendary" and .14 or (card.rarity=="Epic" and .10 or .055))
    local centerSeal=c:CreateTexture(nil,"BACKGROUND",nil,-1);centerSeal:SetTexture(ORNAMENT_SEAL);centerSeal:SetSize(w*.72,w*.72);centerSeal:SetPoint("CENTER",0,-h*.18);centerSeal:SetVertexColor(rc[1],rc[2],rc[3],.08)
    AddCardCornerChrome(c,card,.30)
    local crown=c:CreateTexture(nil,"BORDER");crown:SetTexture(WHITE);crown:SetPoint("TOPLEFT",10,-8);crown:SetPoint("TOPRIGHT",-10,-8);crown:SetHeight(3);crown:SetColorTexture(rc[1],rc[2],rc[3],.48)
    local foot=c:CreateTexture(nil,"BORDER");foot:SetTexture(WHITE);foot:SetPoint("BOTTOMLEFT",11,8);foot:SetPoint("BOTTOMRIGHT",-11,8);foot:SetHeight(2);foot:SetColorTexture(.95,.70,.30,.22)
    local artFrame=CreateFrame("Frame",nil,c,"BackdropTemplate");artFrame:SetPoint("TOPLEFT",12,-13);artFrame:SetPoint("TOPRIGHT",-12,-13);artFrame:SetHeight(math.min(h*.42,w*.70))
    artFrame:SetBackdrop({bgFile=HS_WOOD,tile=true,tileSize=64,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=6,insets={left=2,right=2,top=2,bottom=2}});artFrame:SetBackdropColor(.13,.066,.028,1);artFrame:SetBackdropBorderColor(.58,.37,.19,.95);artFrame:SetClipsChildren(true)
    local art=artFrame:CreateTexture(nil,"ARTWORK");art:SetTexture(card.art or card.icon or "Interface\\Icons\\INV_Misc_QuestionMark");art:SetPoint("TOPLEFT",4,-4);art:SetPoint("BOTTOMRIGHT",-4,4);art:SetTexCoord(.13,.87,.11,.89)
    local gem=MakeManaGem(c,card.cost,math.max(24,math.floor(w*.25)));gem:SetPoint("TOPLEFT",-2,2);gem:SetFrameLevel(c:GetFrameLevel()+10)
    local ribbon=CreateFrame("Frame",nil,c,"BackdropTemplate");ribbon:SetPoint("TOPLEFT",8,-h*.50);ribbon:SetPoint("TOPRIGHT",-8,-h*.50);ribbon:SetHeight(h*.15);ribbon:SetBackdrop({bgFile=HS_PARCHMENT,tile=true,tileSize=64,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=6,insets={left=2,right=2,top=2,bottom=2}});ribbon:SetBackdropColor(.83,.67,.43,.99);ribbon:SetBackdropBorderColor(.58,.36,.17,.96)
    local name=CardText(ribbon,Short(card.name,18),math.max(6.7,w*.071),{.16,.07,.025,1},DISPLAY);name:SetPoint("CENTER",0,1);name:SetWidth(w-20);name:SetJustifyH("CENTER");name:SetWordWrap(false)
    local meta=CardText(c,string.upper((card.rarity or "").." · "..(card.type or "CARD")),math.max(5.7,w*.052),rc,DISPLAY);meta:SetPoint("TOPLEFT",12,-h*.70);meta:SetWidth(w-24);meta:SetJustifyH("LEFT")
    if card.type=="Creature" or card.type=="Equipment" then
        local atk=MakeStatBadge(c,card.attack,"attack",w,h);atk:SetPoint("BOTTOMLEFT",4,3)
        local hpv=card.type=="Creature" and card.health or card.durability
        local hp=MakeStatBadge(c,hpv,"health",w,h);hp:SetPoint("BOTTOMRIGHT",-4,3)
    else
        local action=CardText(c,"ACTION",math.max(6,w*.056),{1,.78,.39,1},DISPLAY);action:SetPoint("BOTTOM",0,10)
    end
    local ring=AddOuterHoverRing(c,card,5);c._hoverRing=ring;c._rarityColor=rc
    c:SetScript("OnEnter",function(self)
        self:SetFrameLevel((self._baseLevel or 1)+35);self:SetScale(1.060);shadow:SetAlpha(.42)
        ring:SetBackdropBorderColor(rc[1],rc[2],rc[3],1);if ring.glow then ring.glow:SetVertexColor(rc[1],rc[2],rc[3],.66);ring.glow:SetAlpha(.72) end
        ring:SetAlpha(1);StartCardParallax(self,3.8,2.6)
        if ShowCardInspect then ShowCardInspect(card,self) end
    end)
    c:SetScript("OnLeave",function(self)
        StopCardParallax(self);self:SetFrameLevel(self._baseLevel or 1);self:SetScale(1);shadow:SetAlpha(.28);ring:SetAlpha(0)
        ring:SetBackdropBorderColor(rc[1],rc[2],rc[3],1);if ring.glow then ring.glow:SetVertexColor(rc[1],rc[2],rc[3],.60) end
        if HideCardInspect then HideCardInspect(self) end
    end)
    return c
end

local function MakeCard(parent,card,w,h,compact,opts)
    if compact then return MakeCompactCard(parent,card,w,h) end
    opts=opts or {};w=w or 190;h=h or math.floor(w*1.40)
    local c=CreateFrame("Button",nil,parent,"BackdropTemplate");if not opts.static then Dynamic(c) end;c:SetSize(w,h);c:EnableMouse(not opts.noMouse);c._hsCard=card;c._baseLevel=c:GetFrameLevel()
    local rc=CardColor(card)
    c:SetBackdrop({bgFile=HS_LEATHER,tile=true,tileSize=96,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=11,insets={left=4,right=4,top=4,bottom=4}})
    c:SetBackdropColor(.255,.145,.073,.995);c:SetBackdropBorderColor(rc[1],rc[2],rc[3],.96)
    local shadow=c:CreateTexture(nil,"BACKGROUND",nil,-2);shadow:SetTexture(WHITE);shadow:SetPoint("TOPLEFT",8,-9);shadow:SetPoint("BOTTOMRIGHT",-8,7);shadow:SetColorTexture(.02,.008,.003,.30)
    local inner=c:CreateTexture(nil,"BACKGROUND",nil,-1);inner:SetTexture(HS_PARCHMENT);inner:SetPoint("TOPLEFT",7,-7);inner:SetPoint("BOTTOMRIGHT",-7,7);inner:SetVertexColor(.70,.49,.27,1);inner:SetAlpha(.20)
    local rarityBloom=c:CreateTexture(nil,"BACKGROUND");rarityBloom:SetTexture(CELESTIAL.."starlight_round.tga");rarityBloom:SetBlendMode("ADD");rarityBloom:SetSize(w*1.18,h*.58);rarityBloom:SetPoint("TOP",0,10);rarityBloom:SetVertexColor(rc[1],rc[2],rc[3],card.rarity=="Legendary" and .18 or (card.rarity=="Epic" and .12 or .065))
    local watermark=c:CreateTexture(nil,"BACKGROUND");watermark:SetTexture(ORNAMENT_SEAL);watermark:SetSize(w*.78,w*.78);watermark:SetPoint("CENTER",0,-h*.20);watermark:SetVertexColor(rc[1],rc[2],rc[3],.08)
    AddCardCornerChrome(c,card,.40)
    local topFiligree=c:CreateTexture(nil,"BORDER");topFiligree:SetTexture(ORNAMENT_ROYAL);topFiligree:SetSize(math.min(78,w*.42),math.min(78,w*.42));topFiligree:SetPoint("TOPRIGHT",2,2);topFiligree:SetVertexColor(rc[1],rc[2],rc[3],.18);if topFiligree.SetRotation then topFiligree:SetRotation(3.1415926) end
    local bottomFiligree=c:CreateTexture(nil,"BORDER");bottomFiligree:SetTexture(ORNAMENT_ROYAL);bottomFiligree:SetSize(math.min(68,w*.36),math.min(68,w*.36));bottomFiligree:SetPoint("BOTTOMLEFT",-1,-1);bottomFiligree:SetVertexColor(.96,.73,.35,.14)
    local artFrame=CreateFrame("Frame",nil,c,"BackdropTemplate");artFrame:SetPoint("TOPLEFT",20,-19);artFrame:SetPoint("TOPRIGHT",-20,-19);artFrame:SetHeight(math.min(h*.44,w*.72))
    artFrame:SetBackdrop({bgFile=HS_WOOD,tile=true,tileSize=96,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=8,insets={left=3,right=3,top=3,bottom=3}});artFrame:SetBackdropColor(.12,.055,.022,1);artFrame:SetBackdropBorderColor(.59,.37,.18,.98);artFrame:SetClipsChildren(true)
    local art=artFrame:CreateTexture(nil,"ARTWORK");art:SetTexture(card.art or card.icon or "Interface\\Icons\\INV_Misc_QuestionMark");art:SetPoint("TOPLEFT",5,-5);art:SetPoint("BOTTOMRIGHT",-5,5);art:SetTexCoord(.13,.87,.10,.90)
    local artShade=artFrame:CreateTexture(nil,"OVERLAY");artShade:SetTexture(WHITE);artShade:SetAllPoints(art);artShade:SetColorTexture(.20,.08,.02,.035)
    local gem=MakeManaGem(c,card.cost,math.max(30,math.floor(w*.18)));gem:SetPoint("TOPLEFT",-3,5);gem:SetFrameLevel(c:GetFrameLevel()+10)
    local ribbon=CreateFrame("Frame",nil,c,"BackdropTemplate");ribbon:SetPoint("TOPLEFT",15,-h*.49);ribbon:SetPoint("TOPRIGHT",-15,-h*.49);ribbon:SetHeight(h*.095);ribbon:SetBackdrop({bgFile=HS_PARCHMENT,tile=true,tileSize=64,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=7,insets={left=2,right=2,top=2,bottom=2}});ribbon:SetBackdropColor(.86,.70,.46,.99);ribbon:SetBackdropBorderColor(.60,.38,.18,1)
    local name=CardText(ribbon,card.name,math.max(8.2,w*.052),{.15,.065,.022,1},DISPLAY);name:SetPoint("CENTER",0,1);name:SetWidth(w-36);name:SetJustifyH("CENTER");name:SetWordWrap(false);if name:GetStringWidth()>(w-36) then name:SetFont(DISPLAY,Scaled(math.max(7.8,w*.043)),"") end
    local metaHost=CreateFrame("Frame",nil,c);metaHost:SetPoint("TOPLEFT",20,-h*.60);metaHost:SetPoint("TOPRIGHT",-20,-h*.60);metaHost:SetHeight(20)
    local meta=CardText(metaHost,string.upper((card.rarity or "").."  ·  "..(card.type or "CARD")),math.max(6.3,w*.035),rc,DISPLAY);meta:SetPoint("LEFT",4,0);meta:SetWidth(w-48);meta:SetJustifyH("LEFT")
    local bodyFrame=CreateFrame("Frame",nil,c,"BackdropTemplate");bodyFrame:SetPoint("TOPLEFT",18,-h*.68);bodyFrame:SetPoint("BOTTOMRIGHT",-18,h*.15);bodyFrame:SetBackdrop({bgFile=HS_PARCHMENT,tile=true,tileSize=64,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=7,insets={left=3,right=3,top=3,bottom=3}});bodyFrame:SetBackdropColor(.80,.69,.49,.99);bodyFrame:SetBackdropBorderColor(.52,.32,.15,.80)
    local body=CardText(bodyFrame,card.text or "",math.max(7.6,w*.040),{.18,.085,.03,1});body:SetPoint("TOPLEFT",8,-7);body:SetPoint("BOTTOMRIGHT",-8,7);body:SetJustifyH("CENTER");body:SetJustifyV("MIDDLE");body:SetWordWrap(true)
    if card.type=="Creature" then local atk=MakeStatBadge(c,card.attack,"attack",w,h);atk:SetPoint("BOTTOMLEFT",5,3);local hp=MakeStatBadge(c,card.health,"health",w,h);hp:SetPoint("BOTTOMRIGHT",-5,3)
    elseif card.type=="Equipment" then local atk=MakeStatBadge(c,card.attack,"attack",w,h);atk:SetPoint("BOTTOMLEFT",5,3);local hp=MakeStatBadge(c,card.durability,"health",w,h);hp:SetPoint("BOTTOMRIGHT",-5,3) end
    if opts.count and tonumber(opts.count) then local own=CreateFrame("Frame",nil,c,"BackdropTemplate");own:SetSize(math.max(30,w*.16),math.max(21,h*.068));own:SetPoint("TOPRIGHT",-7,-7);own:SetBackdrop({bgFile=HS_LEATHER,tile=true,tileSize=64,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=7,insets={left=2,right=2,top=2,bottom=2}});own:SetBackdropColor(.20,.10,.045,.98);own:SetBackdropBorderColor(.68,.43,.21,.92);local ot=Text(own,"×"..tostring(opts.count),math.max(7.2,w/27),P().pale,DISPLAY);ot:SetPoint("CENTER") end
    local ring=AddOuterHoverRing(c,card,6)
    if not opts.noHover then
        c:SetScript("OnEnter",function(self)self:SetFrameLevel((self._baseLevel or 1)+40);self:SetScale(1.060);shadow:SetAlpha(.44);ring:SetAlpha(1);StartCardParallax(self,4.2,2.8);if ShowCardInspect then ShowCardInspect(card,self) end end)
        c:SetScript("OnLeave",function(self)StopCardParallax(self);self:SetFrameLevel(self._baseLevel or 1);self:SetScale(1);shadow:SetAlpha(.30);ring:SetAlpha(0);if HideCardInspect then HideCardInspect(self) end end)
    end
    return c
end

local MECHANIC_HELP={
    Deploy="Triggers when this card is played from your hand.",
    Taunt="Enemies must attack a Taunt creature before other available targets.",
    Rush="Can attack immediately after being deployed.",
    Lifesteal="Damage dealt by this card restores the same amount of Health to your hero.",
    Deadly="Any damage this creature deals to another creature destroys that creature.",
}
local function CardMechanics(card)
    local out={}
    if not card then return out end
    local text=tostring(card.text or ""):lower()
    local function has(pattern) return text:find(pattern,1,true)~=nil end
    -- Prefer explicit engine flags, but also read the printed keyword so older
    -- cards and imported card definitions still receive the same help tooltip.
    if card.battlecry or has("deploy:") or has("deploy.") then out[#out+1]="Deploy" end
    if card.taunt or has("taunt") then out[#out+1]="Taunt" end
    if card.charge or has("rush") then out[#out+1]="Rush" end
    if card.lifesteal or has("lifesteal") then out[#out+1]="Lifesteal" end
    if card.deadly or has("deadly") then out[#out+1]="Deadly" end
    return out
end
local function EnsureCardInspect()
    if cardInspect then return cardInspect end
    local f=CreateFrame("Frame",nil,UIParent,"BackdropTemplate")
    f:SetSize(540,304);f:SetFrameStrata("TOOLTIP");f:SetFrameLevel(9000);f:SetClampedToScreen(true);f:EnableMouse(false)
    Backdrop(f,P().gold,.995);f:SetBackdropColor(.040,.018,.011,.992)
    local glow=f:CreateTexture(nil,"BACKGROUND",nil,-1);glow:SetTexture(CELESTIAL.."starlight_glow.tga");glow:SetBlendMode("ADD");glow:SetPoint("TOPLEFT",-22,22);glow:SetPoint("BOTTOMRIGHT",22,-22);glow:SetVertexColor(1,.60,.20,.12)
    f.host=CreateFrame("Frame",nil,f);f.host:SetPoint("TOPLEFT",10,-10);f.host:SetPoint("BOTTOMRIGHT",-10,10)
    f:Hide();cardInspect=f;return f
end

ShowCardInspect=function(card,anchor)
    if not card or not anchor or not anchor:IsShown() then return end
    local mechanics=CardMechanics(card)
    local f=EnsureCardInspect();f._owner=anchor;Clear(f.host)
    local hasMechanics=#mechanics>0
    f:SetSize(hasMechanics and 540 or 238,304)
    -- Always show a readable, full card.  During a match this lets the hand stay
    -- compact enough for seven cards while hover provides every printed detail.
    local preview=MakeCard(f.host,card,190,266,false,{noHover=true,noMouse=true});preview:SetPoint("LEFT",8,0);preview:SetFrameLevel(f:GetFrameLevel()+3)
    if hasMechanics then
        local divider=f.host:CreateTexture(nil,"BORDER");divider._hsDynamic=true;divider:SetTexture(WHITE);divider:SetSize(1,250);divider:SetPoint("LEFT",preview,"RIGHT",14,0);divider:SetColorTexture(1,.68,.28,.34)
        local title=DText(f.host,"MECHANICS",11.5,P().gold,DISPLAY);title:SetPoint("TOPLEFT",224,-10)
        local intro=DText(f.host,"Keywords on this card",7.8,P().muted,FONT);intro:SetPoint("TOPLEFT",224,-32)
        local y=-60
        for _,key in ipairs(mechanics) do
            local rc=CardColor(card)
            local name=DText(f.host,string.upper(key),10.4,rc,DISPLAY);name:SetPoint("TOPLEFT",224,y);name:SetWidth(104);name:SetJustifyH("LEFT")
            local desc=DText(f.host,MECHANIC_HELP[key] or "Special card mechanic.",8.25,P().pale,FONT);desc:SetPoint("TOPLEFT",224,y-18);desc:SetWidth(286);desc:SetJustifyH("LEFT");desc:SetWordWrap(true)
            y=y-56
        end
    end
    f:ClearAllPoints();local ax=anchor:GetCenter();local sw=UIParent:GetWidth() or 1920
    if ax and ax>sw*.56 then f:SetPoint("RIGHT",anchor,"LEFT",-15,0) else f:SetPoint("LEFT",anchor,"RIGHT",15,0) end
    f:SetAlpha(1);f:Show();f:Raise()
end
HideCardInspect=function(anchor)
    if not cardInspect then return end
    if anchor and cardInspect._owner~=anchor then return end
    cardInspect._owner=nil;cardInspect:Hide()
end

local function Prompt(title,default,callback)
    local sh=CreateFrame("Frame",nil,UIParent,"BackdropTemplate");sh:SetAllPoints();sh:SetFrameStrata("TOOLTIP");sh:EnableMouse(true);sh:SetBackdrop({bgFile=WHITE});sh:SetBackdropColor(0,0,0,.55)
    local p=CreateFrame("Frame",nil,sh,"BackdropTemplate");p:SetSize(430,150);p:SetPoint("CENTER");Backdrop(p,P().gold,.99)
    local h=Text(p,title,14,P().gold,DISPLAY);h:SetPoint("TOP",0,-18)
    local e=CreateFrame("EditBox",nil,p,"InputBoxTemplate");e:SetSize(360,30);e:SetPoint("TOP",0,-54);e:SetAutoFocus(true);e:SetText(default or "");e:HighlightText()
    local ok=Btn(p,"Confirm",110,28,true);ok:SetPoint("BOTTOMRIGHT",-22,18);local cancel=Btn(p,"Cancel",100,28,false);cancel:SetPoint("RIGHT",ok,"LEFT",-8,0)
    cancel:SetScript("OnClick",function()sh:Hide()end);ok:SetScript("OnClick",function()local v=e:GetText();sh:Hide();callback(v)end);e:SetScript("OnEnterPressed",function()ok:Click()end)
end

local function EnsureMain()
    if frame then return frame end
    local f=CreateFrame("Frame","RPAL_Hearthstone",UIParent,"BackdropTemplate")
    frame=f
    f:SetSize(1240,790);f:SetPoint("CENTER");if RPAL.ApplyUILayer then RPAL.ApplyUILayer(f,"WORKSPACE")else f:SetFrameStrata("DIALOG");f:SetFrameLevel(320);f:SetToplevel(true)end;f:SetClampedToScreen(true)
    f:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=16,insets={left=4,right=4,top=4,bottom=4}})
    f:SetBackdropColor(.045,.020,.013,.998);f:SetBackdropBorderColor(.88,.57,.22,1)
    f:Hide();if RPAL.RegisterEscapeFrame then RPAL.RegisterEscapeFrame(f) end
    f:SetMovable(true);f:EnableMouse(true);f:RegisterForDrag("LeftButton");f:SetScript("OnDragStart",function(self)if IsAltKeyDown() then self:StartMoving() end end);f:SetScript("OnDragStop",function(self)self:StopMovingOrSizing()end)

    local base=f:CreateTexture(nil,"BACKGROUND",nil,-8);base:SetTexture(WHITE);base:SetPoint("TOPLEFT",6,-6);base:SetPoint("BOTTOMRIGHT",-6,6);base:SetColorTexture(.075,.034,.021,1)
    local topWash=f:CreateTexture(nil,"BACKGROUND",nil,-7);topWash:SetTexture(WHITE);topWash:SetPoint("TOPLEFT",8,-8);topWash:SetPoint("TOPRIGHT",-8,-8);topWash:SetHeight(150)
    if topWash.SetGradientAlpha then topWash:SetGradientAlpha("VERTICAL",.30,.105,.035,.88,.075,.026,.015,.08) else topWash:SetColorTexture(.20,.07,.025,.48) end
    local centerGlow=f:CreateTexture(nil,"BACKGROUND",nil,-6);centerGlow:SetTexture(CELESTIAL.."starlight_glow.tga");centerGlow:SetBlendMode("ADD");centerGlow:SetSize(1040,700);centerGlow:SetPoint("CENTER",0,-28);centerGlow:SetVertexColor(1,.49,.14,.055)
    local seal=f:CreateTexture(nil,"BACKGROUND",nil,-5);seal:SetTexture(ORNAMENT_SEAL);seal:SetSize(590,590);seal:SetPoint("CENTER",0,-36);seal:SetVertexColor(1,.66,.24,.045)
    local headerLine=f:CreateTexture(nil,"BORDER");headerLine:SetTexture(WHITE);headerLine:SetPoint("TOPLEFT",22,-70);headerLine:SetPoint("TOPRIGHT",-22,-70);headerLine:SetHeight(2);headerLine:SetColorTexture(.95,.64,.25,.70)
    local headerGlow=f:CreateTexture(nil,"BORDER");headerGlow:SetTexture(CELESTIAL.."starlight_comet_1.tga");headerGlow:SetBlendMode("ADD");headerGlow:SetSize(650,20);headerGlow:SetPoint("TOP",0,-63);headerGlow:SetVertexColor(1,.68,.25,.20)

    local function Corner(point,x,y,rotation)
        local c=f:CreateTexture(nil,"BORDER");c:SetTexture(ORNAMENT_ROYAL);c:SetSize(132,132);c:SetPoint(point,x,y);c:SetVertexColor(1,.70,.30,.34);if rotation and c.SetRotation then c:SetRotation(rotation) end;return c
    end
    Corner("TOPLEFT",5,-5,0);Corner("TOPRIGHT",-5,-5,1.5707963);Corner("BOTTOMRIGHT",-5,5,3.1415926);Corner("BOTTOMLEFT",5,5,4.712389)

    f.title=Text(f,"UNOFFICIAL HEARTHSTONE",24,{1,.78,.38,1},TITLE_FONT);f.title:SetPoint("TOPLEFT",34,-20);f.title:SetShadowColor(.12,.035,.008,1);f.title:SetShadowOffset(2,-2)
    f.sub=Text(f,"ODYN TAVERN TABLE",8.4,{.82,.63,.42,1},FONT);f.sub:SetPoint("TOPLEFT",37,-53)

    local tokenPlate=CreateFrame("Frame",nil,f,"BackdropTemplate");tokenPlate:SetSize(180,42);tokenPlate:SetPoint("TOPRIGHT",-58,-19);tokenPlate:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=10,insets={left=2,right=2,top=2,bottom=2}});tokenPlate:SetBackdropColor(.11,.052,.022,.98);tokenPlate:SetBackdropBorderColor(.82,.52,.18,.95)
    local coin=tokenPlate:CreateTexture(nil,"ARTWORK");coin:SetTexture(TB.."UI_TravelBook_IconCoin01a.tga");coin:SetSize(28,28);coin:SetPoint("LEFT",10,0);coin:SetVertexColor(1,.78,.30,1)
    f.tokens=Text(tokenPlate,"",10.5,{1,.82,.42,1},FONT);f.tokens:SetPoint("LEFT",coin,"RIGHT",8,0);f.tokens:SetWidth(126);f.tokens:SetJustifyH("LEFT")

    local close=CreateFrame("Button",nil,f,"BackdropTemplate");close:SetSize(34,30);close:SetPoint("TOPRIGHT",-16,-16);close:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=8,insets={left=2,right=2,top=2,bottom=2}});close:SetBackdropColor(.08,.028,.018,.98);close:SetBackdropBorderColor(.55,.30,.14,.90);close.label=Text(close,"×",18,{1,.78,.50,1},TITLE_FONT);close.label:SetPoint("CENTER",0,1);close:SetScript("OnEnter",function(self)self:SetBackdropBorderColor(1,.68,.28,1);self:SetBackdropColor(.20,.055,.025,1)end);close:SetScript("OnLeave",function(self)self:SetBackdropBorderColor(.55,.30,.14,.90);self:SetBackdropColor(.08,.028,.018,.98)end);close:SetScript("OnClick",function()f:Hide()end);if RPAL.AttachButtonMotion then RPAL.AttachButtonMotion(close,0.016)end

    f.nav=CreateFrame("Frame",nil,f,"BackdropTemplate");f.nav:SetPoint("TOPLEFT",24,-80);f.nav:SetPoint("TOPRIGHT",-24,-80);f.nav:SetHeight(48);f.nav:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=8,insets={left=2,right=2,top=2,bottom=2}});f.nav:SetBackdropColor(.035,.017,.012,.94);f.nav:SetBackdropBorderColor(.30,.17,.09,.82)
    local navGlow=f.nav:CreateTexture(nil,"BACKGROUND");navGlow:SetTexture(WHITE);navGlow:SetPoint("TOPLEFT",3,-3);navGlow:SetPoint("TOPRIGHT",-3,-3);navGlow:SetHeight(1);navGlow:SetColorTexture(1,.70,.30,.30)

    local tabs={{"play","PLAY"},{"collection","COLLECTION"},{"packs","PACKS"},{"cosmetics","CUSTOMIZATION"},{"history","HISTORY"}}
    f.tabs={}
    for i,t in ipairs(tabs) do
        local key,label=t[1],t[2]
        local b=CreateFrame("Button",nil,f.nav,"BackdropTemplate");b:SetSize(181,36);b:SetPoint("LEFT",12+(i-1)*194,0);b:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=7,insets={left=2,right=2,top=2,bottom=2}});b:SetBackdropColor(.075,.032,.018,.96);b:SetBackdropBorderColor(.31,.18,.10,.86)
        b.label=Text(b,label,9.2,{.84,.72,.58,1},FONT);b.label:SetPoint("CENTER",0,1)
        b.activeLine=b:CreateTexture(nil,"OVERLAY");b.activeLine:SetTexture(WHITE);b.activeLine:SetPoint("BOTTOMLEFT",8,4);b.activeLine:SetPoint("BOTTOMRIGHT",-8,4);b.activeLine:SetHeight(2);b.activeLine:SetColorTexture(1,.70,.28,1);b.activeLine:SetAlpha(0)
        b.activeGlow=b:CreateTexture(nil,"BACKGROUND");b.activeGlow:SetTexture(CELESTIAL.."starlight_glow.tga");b.activeGlow:SetBlendMode("ADD");b.activeGlow:SetPoint("TOPLEFT",-12,12);b.activeGlow:SetPoint("BOTTOMRIGHT",12,-12);b.activeGlow:SetVertexColor(1,.58,.18,.34);b.activeGlow:SetAlpha(0)
        b:SetScript("OnEnter",function(self)if key~=selectedTab then self:SetBackdropColor(.14,.060,.025,1);self:SetBackdropBorderColor(.72,.42,.17,.98);self.activeGlow:SetAlpha(.18) end end)
        b:SetScript("OnLeave",function(self)if key~=selectedTab then self:SetBackdropColor(.075,.032,.018,.96);self:SetBackdropBorderColor(.31,.18,.10,.86);self.activeGlow:SetAlpha(0) end end)
        b:SetScript("OnClick",function()selectedTab=key;RPAL.RefreshHearthstone()end)
        f.tabs[key]=b
    end

    f.contentShell=CreateFrame("Frame",nil,f,"BackdropTemplate");f.contentShell:SetPoint("TOPLEFT",24,-140);f.contentShell:SetPoint("BOTTOMRIGHT",-24,22);f.contentShell:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=12,insets={left=3,right=3,top=3,bottom=3}});f.contentShell:SetBackdropColor(.055,.027,.018,.975);f.contentShell:SetBackdropBorderColor(.40,.235,.12,.94)
    local shellTop=f.contentShell:CreateTexture(nil,"BACKGROUND");shellTop:SetTexture(WHITE);shellTop:SetPoint("TOPLEFT",4,-4);shellTop:SetPoint("TOPRIGHT",-4,-4);shellTop:SetHeight(72);if shellTop.SetGradientAlpha then shellTop:SetGradientAlpha("VERTICAL",.20,.085,.030,.54,.055,.027,.018,.02) else shellTop:SetColorTexture(.12,.05,.02,.24) end
    f.content=CreateFrame("Frame",nil,f.contentShell);f.content:SetPoint("TOPLEFT",10,-10);f.content:SetPoint("BOTTOMRIGHT",-10,10)
    return f
end

local function RefreshTabVisuals()
    local visible={"play","collection","packs","cosmetics","history"}
    if not frame or not frame.tabs or not frame.nav then return end
    if not frame.tabs[selectedTab] then selectedTab="play" end
    local navWidth=(frame.nav.GetWidth and frame.nav:GetWidth()) or 1170
    local gap=10;local width=math.floor((navWidth-24-((#visible-1)*gap))/#visible)
    for idx,key in ipairs(visible) do
        local b=frame.tabs[key]
        if b then
            b:SetShown(true)
            b:ClearAllPoints();b:SetSize(width,36);b:SetPoint("LEFT",12+(idx-1)*(width+gap),0)
            local on=key==selectedTab
            if on then
                b:SetBackdropColor(.20,.082,.026,.99);b:SetBackdropBorderColor(1,.67,.25,1);b.label:SetTextColor(1,.84,.48,1);b.activeLine:SetAlpha(1);b.activeGlow:SetAlpha(.42)
            else
                b:SetBackdropColor(.075,.032,.018,.96);b:SetBackdropBorderColor(.31,.18,.10,.86);b.label:SetTextColor(.84,.72,.58,1);b.activeLine:SetAlpha(0);b.activeGlow:SetAlpha(0)
            end
        end
    end
end

local function RenderPlay(content)
    local state=HS.GetState();local deck=HS.GetActiveDeck();local ability=HS.GetActiveAbility()
    local hero=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));hero:SetSize(520,180);hero:SetPoint("TOPLEFT",8,-8);Backdrop(hero,P().gold,.98);DressMenuPanel(hero,P().gold,.055)
    local portrait=hero:CreateTexture(nil,"ARTWORK");portrait:SetSize(104,104);portrait:SetPoint("LEFT",18,6);if SetPortraitTexture then pcall(SetPortraitTexture,portrait,"player") else portrait:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") end
    local name=DText(hero,(RPAL.DisplayName and RPAL.DisplayName()) or UnitName("player"),20,P().gold,DISPLAY);name:SetPoint("TOPLEFT",140,-26)
    local info=DText(hero,"Your RP character is always your Unofficial Hearthstone hero.",9,P().muted);info:SetPoint("TOPLEFT",142,-57)
    local abilityTitle=DText(hero,"HERO ABILITY",8,P().gold);abilityTitle:SetPoint("TOPLEFT",142,-86)
    local ab=DText(hero,ability.name.." · "..ability.cost.." Mana",11,P().pale,DISPLAY);ab:SetPoint("TOPLEFT",142,-105)
    local abt=DText(hero,ability.text,8.5,P().muted);abt:SetPoint("TOPLEFT",142,-128);abt:SetWidth(340)
    local chooseAbility=DBtn(hero,"Choose Ability",116,28,false);chooseAbility:SetPoint("BOTTOMRIGHT",-12,12);chooseAbility:SetScript("OnClick",function()RPAL.OpenHearthstoneAbilityPicker()end)

    local setup=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));setup:SetSize(560,180);setup:SetPoint("TOPRIGHT",-8,-8);Backdrop(setup,P().border,.98);DressMenuPanel(setup,P().accent,.045)
    local sh=DText(setup,"TABLE SETUP",11,P().gold,DISPLAY);sh:SetPoint("TOPLEFT",16,-14)
    local deckLabel=DText(setup,"ACTIVE DECK",7.5,P().muted);deckLabel:SetPoint("TOPLEFT",16,-44)
    local dn=DText(setup,(deck and deck.name or "No Deck").."  ·  "..tostring(deck and #deck.cards or 0).." / 20",12,P().pale,DISPLAY);dn:SetPoint("TOPLEFT",16,-61)
    local vd,err=HS.ValidateDeck(deck);local valid=DText(setup,vd and "READY TO PLAY" or tostring(err),8,vd and {0.4,1,.55} or {1,.4,.4});valid:SetPoint("TOPLEFT",16,-85)
    local stake=DText(setup,"STAKED CARDS  ·  "..tostring(#(state.pendingStakes or {})),8,P().gold);stake:SetPoint("TOPLEFT",16,-111)
    local stakeNames={};for _,id in ipairs(state.pendingStakes or {}) do local c=HS.GetCard(id);if c then stakeNames[#stakeNames+1]=c.name end end
    local sn=DText(setup,#stakeNames>0 and Short(table.concat(stakeNames,", "),54) or "No cards at stake",8.5,P().muted);sn:SetPoint("TOPLEFT",16,-129);sn:SetWidth(330)
    local changeDeck=DBtn(setup,"Change Deck",100,28,false);changeDeck:SetPoint("TOPRIGHT",-14,-42);changeDeck:SetScript("OnClick",function()selectedTab="collection";selectedDeckID=(HS.GetState() or {}).activeDeck;RPAL.RefreshHearthstone()end)
    local stakes=DBtn(setup,"Stake Cards",100,28,false);stakes:SetPoint("TOPRIGHT",-14,-76);stakes:SetScript("OnClick",function()RPAL.OpenHearthstoneStakePicker()end)

    local play=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));play:SetPoint("TOPLEFT",8,-202);play:SetPoint("BOTTOMRIGHT",-8,8);Backdrop(play,P().border,.98);DressMenuPanel(play,P().gold,.038)
    local ph=DText(play,"PLAY ONLINE",16,P().gold,DISPLAY);ph:SetPoint("TOPLEFT",18,-16)
    local help=DText(play,"Target another ODYN user, then challenge them. The challenger hosts the authoritative table state; only actions and compact state updates travel over addon messages.",9,P().muted);help:SetPoint("TOPLEFT",18,-45);help:SetWidth(760);help:SetWordWrap(true)
    local challenge=DBtn(play,"Challenge Target",168,38,true);challenge:SetPoint("TOPLEFT",18,-94);challenge:SetScript("OnClick",function()local ok,msg=HS.ChallengeTarget(false);ShowStatus(msg or (ok and "Challenge sent." or "Could not challenge."))end)
    local spectate=DBtn(play,"Spectate Target",154,38,false);spectate:SetPoint("LEFT",challenge,"RIGHT",10,0);spectate:SetScript("OnClick",function()local ok,msg=HS.RequestSpectateTarget();ShowStatus(msg or (ok and "Request sent." or "Could not spectate."))end)
    local dm=DBtn(play,"DM Table → Target",166,38,false);dm:SetPoint("LEFT",spectate,"RIGHT",10,0);dm:SetScript("OnClick",function()
        Prompt("NPC Opponent Name",tostring(state.dmHeroName or "Tavern Challenger"),function(v)
            state.dmHeroName=(v and v~="") and v or "Tavern Challenger";local ok,msg=HS.ChallengeTarget(true);ShowStatus(msg or (ok and "DM table invitation sent." or "Could not start DM table."))
        end)
    end)
    local pausedPractice=HS.GetPausedPracticeMatch and HS.GetPausedPracticeMatch() or nil
    local practice=DBtn(play,pausedPractice and "RESUME PRACTICE" or "PRACTICE MATCH",186,42,true);practice:SetPoint("TOPLEFT",18,-154);practice:SetScript("OnClick",function()local ok,msg;if pausedPractice and HS.ResumePracticeMatch then ok,msg=HS.ResumePracticeMatch() else ok,msg=HS.StartPracticeMatch(HS.GetPracticeDifficulty and HS.GetPracticeDifficulty() or "normal") end;if not ok then ShowStatus(msg or "Could not start practice match.") elseif msg then ShowStatus(msg) end end)
    if pausedPractice then local fresh=DBtn(play,"NEW PRACTICE",118,26,false);fresh:SetPoint("TOPLEFT",18,-202);fresh:SetScript("OnClick",function()if HS.DiscardPausedMatch then HS.DiscardPausedMatch(pausedPractice.id) end;local ok,msg=HS.StartPracticeMatch(HS.GetPracticeDifficulty and HS.GetPracticeDifficulty() or "normal");if not ok then ShowStatus(msg or "Could not start practice match.") end end) end
    local diffLabel=DText(play,"BOT DIFFICULTY",8.0,P().muted,DISPLAY);diffLabel:SetPoint("TOPLEFT",222,-147)
    local currentDiff=(HS.GetPracticeDifficulty and HS.GetPracticeDifficulty()) or "normal"
    local dx=222
    for _,entry in ipairs({{"easy","EASY"},{"normal","NORMAL"},{"hard","HARD"}}) do local id,label=entry[1],entry[2];local b=DBtn(play,label,82,28,currentDiff==id);b:SetPoint("TOPLEFT",dx,-166);dx=dx+88;b:SetScript("OnClick",function()if HS.SetPracticeDifficulty then HS.SetPracticeDifficulty(id) end;RPAL.RefreshHearthstone()end) end
    local practiceText=DText(play,(pausedPractice and "A paused bot match is ready to continue.  ·  " or "").."Easy makes mistakes · Normal plays for tempo · Hard values lethal, trades and resource efficiency. Practice never awards match currency.",8.8,P().muted);practiceText:SetPoint("TOPLEFT",498,-151);practiceText:SetWidth(460);practiceText:SetWordWrap(true)
end

local function RenderCollection(content,clickHandler)
    local state=HS.GetState();selectedDeckID=selectedDeckID or state.activeDeck
    local selected=selectedDeckID and state.decks[selectedDeckID] or nil
    local decks,editor
    if not selected and state.activeDeck and state.decks[state.activeDeck] then selectedDeckID=state.activeDeck;selected=state.decks[state.activeDeck] end
    local owned=0;for _,c in ipairs(HS.CARDS) do if (state.collection[c.id] or 0)>0 then owned=owned+1 end end

    local h=DText(content,"COLLECTION",18,P().gold,DISPLAY);h:SetPoint("TOPLEFT",8,-7)
    local hintText=selected and ("Editing  "..Short(selected.name,26).."  ·  Click or drag a card into the deck") or "Select a deck on the right to begin editing"
    local meta=DText(content,tostring(owned).." / "..tostring(#HS.CARDS).." unique cards  ·  "..hintText,10,P().muted);meta:SetPoint("TOPLEFT",10,-34);meta:SetWidth(900)

    local searchLabel=DText(content,"SEARCH",8.5,P().muted);searchLabel:SetPoint("TOPLEFT",10,-60)
    local search=Dynamic(CreateFrame("EditBox",nil,content,"InputBoxTemplate"));search:SetSize(210,28);search:SetPoint("LEFT",searchLabel,"RIGHT",9,0);search:SetAutoFocus(false);search:SetText(collectionSearch or "")
    search:SetScript("OnTextChanged",function(self,user)if user then collectionSearch=self:GetText() or "" end end)
    search:SetScript("OnEnterPressed",function(self)collectionSearch=self:GetText() or "";collectionPage=1;collectionScrollY=0;self:ClearFocus();RPAL.RefreshHearthstone()end)
    search:SetScript("OnEscapePressed",function(self)self:ClearFocus()end)

    local filterX=330
    for _,kind in ipairs({"All","Creature","Action","Equipment"}) do
        local k=kind;local b=DBtn(content,k,88,26,collectionTypeFilter==k);b:SetPoint("TOPLEFT",filterX,-52);filterX=filterX+94
        b:SetScript("OnClick",function()collectionTypeFilter=k;collectionPage=1;collectionScrollY=0;RPAL.RefreshHearthstone()end)
    end
    local apply=DBtn(content,"Apply",62,26,false);apply:SetPoint("TOPLEFT",708,-52);apply:SetScript("OnClick",function()collectionSearch=search:GetText() or "";collectionPage=1;collectionScrollY=0;RPAL.RefreshHearthstone()end)

    local rarityOrder={"All","Common","Rare","Epic","Legendary"}
    local manaOrder={"All","0-2","3-4","5-6","7+"}
    local sortOrder={"Mana","Name","Rarity","Type","Owned"}
    local function NextValue(current,list)
        local idx=1;for i,v in ipairs(list) do if v==current then idx=i break end end
        return list[(idx%#list)+1]
    end
    local rarityBtn=DBtn(content,"RARITY: "..string.upper(collectionRarityFilter),132,25,false);rarityBtn:SetPoint("TOPLEFT",10,-84);rarityBtn:SetScript("OnClick",function()collectionRarityFilter=NextValue(collectionRarityFilter,rarityOrder);collectionPage=1;collectionScrollY=0;RPAL.RefreshHearthstone()end)
    local manaBtn=DBtn(content,"MANA: "..string.upper(collectionManaFilter),114,25,false);manaBtn:SetPoint("LEFT",rarityBtn,"RIGHT",6,0);manaBtn:SetScript("OnClick",function()collectionManaFilter=NextValue(collectionManaFilter,manaOrder);collectionPage=1;collectionScrollY=0;RPAL.RefreshHearthstone()end)
    local sortBtn=DBtn(content,"SORT: "..string.upper(collectionSortKey),122,25,false);sortBtn:SetPoint("LEFT",manaBtn,"RIGHT",6,0);sortBtn:SetScript("OnClick",function()collectionSortKey=NextValue(collectionSortKey,sortOrder);collectionPage=1;collectionScrollY=0;RPAL.RefreshHearthstone()end)
    local direction=DBtn(content,collectionSortAsc and "ASC ↑" or "DESC ↓",74,25,false);direction:SetPoint("LEFT",sortBtn,"RIGHT",6,0);direction:SetScript("OnClick",function()collectionSortAsc=not collectionSortAsc;collectionPage=1;collectionScrollY=0;RPAL.RefreshHearthstone()end)
    local reset=DBtn(content,"RESET FILTERS",112,25,false);reset:SetPoint("LEFT",direction,"RIGHT",6,0);reset:SetScript("OnClick",function()collectionTypeFilter="All";collectionRarityFilter="All";collectionManaFilter="All";collectionSortKey="Mana";collectionSortAsc=true;collectionSearch="";collectionPage=1;collectionScrollY=0;RPAL.RefreshHearthstone()end)

    local library=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));library:SetPoint("TOPLEFT",4,-118);library:SetPoint("BOTTOMRIGHT",-374,4);Backdrop(library,P().border,.985);library:SetBackdropColor(.11,.052,.027,.985);DressMenuPanel(library,P().accent,.028)
    local child=Dynamic(CreateFrame("Frame",nil,library));child:SetPoint("TOPLEFT",10,-10);child:SetPoint("BOTTOMRIGHT",-10,42)

    local rarityRank={Common=1,Rare=2,Epic=3,Legendary=4}
    local function ManaOK(cost)
        cost=tonumber(cost) or 0
        if collectionManaFilter=="All" then return true end
        if collectionManaFilter=="0-2" then return cost<=2 end
        if collectionManaFilter=="3-4" then return cost>=3 and cost<=4 end
        if collectionManaFilter=="5-6" then return cost>=5 and cost<=6 end
        if collectionManaFilter=="7+" then return cost>=7 end
        return true
    end
    local cards={};local q=tostring(collectionSearch or ""):lower()
    for _,card in ipairs(HS.CARDS) do
        local count=tonumber(state.collection[card.id]) or 0
        local name=tostring(card.name or ""):lower();local text=tostring(card.text or ""):lower()
        local typeOK=(collectionTypeFilter=="All" or tostring(card.type or "")==collectionTypeFilter)
        local rarityOK=(collectionRarityFilter=="All" or tostring(card.rarity or "")==collectionRarityFilter)
        local searchOK=(q=="" or name:find(q,1,true) or text:find(q,1,true))
        if count>0 and typeOK and rarityOK and ManaOK(card.cost) and searchOK then cards[#cards+1]=card end
    end
    table.sort(cards,function(a,b)
        local av,bv
        if collectionSortKey=="Name" then av,bv=tostring(a.name or ""),tostring(b.name or "")
        elseif collectionSortKey=="Rarity" then av,bv=rarityRank[a.rarity] or 0,rarityRank[b.rarity] or 0
        elseif collectionSortKey=="Type" then av,bv=tostring(a.type or ""),tostring(b.type or "")
        elseif collectionSortKey=="Owned" then av,bv=tonumber(state.collection[a.id]) or 0,tonumber(state.collection[b.id]) or 0
        else av,bv=tonumber(a.cost) or 0,tonumber(b.cost) or 0 end
        if av==bv then
            local ac,bc=tonumber(a.cost) or 0,tonumber(b.cost) or 0
            if ac==bc then
                if collectionSortAsc then return tostring(a.name or "")<tostring(b.name or "") end
                return tostring(a.name or "")>tostring(b.name or "")
            end
            if collectionSortAsc then return ac<bc end
            return ac>bc
        end
        if collectionSortAsc then return av<bv end
        return av>bv
    end)

    -- Page-by-page library: no scrollbar jumps while editing a deck.
    local cols,pageSize=4,8
    local totalPages=math.max(1,math.ceil(#cards/pageSize));collectionPage=math.max(1,math.min(totalPages,collectionPage or 1))
    local footer=Dynamic(CreateFrame("Frame",nil,library));footer:SetPoint("BOTTOMLEFT",10,8);footer:SetPoint("BOTTOMRIGHT",-10,8);footer:SetHeight(28)
    local pageInfo=DText(footer,string.format("PAGE  %d / %d   ·   %d CARDS",collectionPage,totalPages,#cards),8.2,P().muted,DISPLAY);pageInfo:SetPoint("CENTER",0,0)
    local prev=DBtn(footer,"‹",34,24,false);prev:SetPoint("RIGHT",pageInfo,"LEFT",-12,0);prev:SetEnabled(collectionPage>1);prev:SetScript("OnClick",function()collectionPage=math.max(1,collectionPage-1);RPAL.RefreshHearthstone()end)
    local nextb=DBtn(footer,"›",34,24,false);nextb:SetPoint("LEFT",pageInfo,"RIGHT",12,0);nextb:SetEnabled(collectionPage<totalPages);nextb:SetScript("OnClick",function()collectionPage=math.min(totalPages,collectionPage+1);RPAL.RefreshHearthstone()end)
    if #cards==0 then local empty=DText(child,"No cards match these filters.",10,P().muted,DISPLAY);empty:SetPoint("CENTER",0,30) end
    local first=(collectionPage-1)*pageSize+1;local last=math.min(#cards,first+pageSize-1);local cw,ch=194,225
    for index=first,last do
        local card=cards[index];local localIndex=index-first;local count=tonumber(state.collection[card.id]) or 0;local row=math.floor(localIndex/cols);local col=localIndex%cols;local cardRef=card
        local cf=MakeCard(child,cardRef,166,224,false,{count=count});cf:SetPoint("TOPLEFT",12+col*cw,-2-row*ch)
        cf:RegisterForDrag("LeftButton")
        cf:SetScript("OnClick",function()
            if clickHandler then clickHandler(cardRef);return end
            if selected then local ok,err=HS.AddCardToDeck(selected.id,cardRef.id);if not ok then ShowStatus(err) end;RPAL.RefreshHearthstone() else ShowStatus("Select a deck on the right first.") end
        end)
        cf:SetScript("OnDragStart",function(self)
            if clickHandler or not selected then return end
            self._hsDragging=true;self:SetAlpha(.58);StartCollectionDragGhost(cardRef,"add")
        end)
        cf:SetScript("OnDragStop",function(self)
            if not self._hsDragging then return end
            self._hsDragging=nil;self:SetAlpha(1);StopCollectionDragGhost()
            if selected and (MouseOverFrame(editor) or MouseOverFrame(decks)) then
                local ok,err=HS.AddCardToDeck(selected.id,cardRef.id);if not ok then ShowStatus(err) end;RPAL.RefreshHearthstone()
            end
        end)
    end

    decks=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));decks:SetPoint("TOPRIGHT",-4,-4);decks:SetPoint("BOTTOMRIGHT",-4,4);decks:SetWidth(360);Backdrop(decks,P().gold,.99);decks:SetBackdropColor(.10,.045,.022,.99);DressMenuPanel(decks,P().gold,.050)
    local dh=DText(decks,"DECKS",15,P().gold,DISPLAY);dh:SetPoint("TOPLEFT",14,-13)
    local newDeck=DBtn(decks,"+ New",78,26,false);newDeck:SetPoint("TOPRIGHT",-12,-9);newDeck:SetScript("OnClick",function()
        Prompt("New Deck","New Deck",function(v)
            local d=HS.CreateDeck(v)
            if d and d.id then selectedDeckID=d.id;ShowStatus("Created "..tostring(d.name or "New Deck")..".") else ShowStatus("Could not create that deck.") end
            RPAL.RefreshHearthstone()
        end)
    end)

    local deckScroll=Dynamic(CreateFrame("ScrollFrame",nil,decks,"UIPanelScrollFrameTemplate"));deckScroll:SetPoint("TOPLEFT",8,-43);deckScroll:SetPoint("TOPRIGHT",-27,-43);deckScroll:SetHeight(176)
    if RPAL.SkinScrollFrame then RPAL.SkinScrollFrame(deckScroll) end
    local deckChild=Dynamic(CreateFrame("Frame",nil,deckScroll));deckChild:SetSize(314,1);deckScroll:SetScrollChild(deckChild)
    local dy=0;local selectedOffset=0
    for _,id in ipairs(state.deckOrder or {}) do
        local d=state.decks[id]
        if d then
            local dRef=d;local row=Dynamic(CreateFrame("Button",nil,deckChild,"BackdropTemplate"));row:SetSize(310,45);row:SetPoint("TOPLEFT",0,-dy)
            row:SetBackdrop({bgFile=HS_LEATHER,tile=true,tileSize=64,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=7,insets={left=2,right=2,top=2,bottom=2}})
            local on=dRef.id==selectedDeckID;local active=dRef.id==state.activeDeck;if on then selectedOffset=dy end
            row:SetBackdropColor(on and .38 or .23,on and .21 or .12,on and .09 or .052,.99);row:SetBackdropBorderColor(active and P().gold[1] or .46,active and P().gold[2] or .28,active and P().gold[3] or .13,on and 1 or .82)
            local name=DText(row,Short(dRef.name,24),10.5,on and P().gold or P().pale,DISPLAY);name:SetPoint("TOPLEFT",10,-8);name:SetWidth(185);name:SetJustifyH("LEFT")
            local cnt=DText(row,tostring(#(dRef.cards or {})).." / 20",9,#(dRef.cards or {})==20 and {.45,1,.58,1} or P().muted);cnt:SetPoint("BOTTOMLEFT",10,7)
            if active then local badge=DText(row,"ACTIVE",8,P().gold,DISPLAY);badge:SetPoint("RIGHT",-10,0) end
            row:SetScript("OnEnter",function(self)self:SetBackdropBorderColor(P().gold[1],P().gold[2],P().gold[3],1) end)
            row:SetScript("OnLeave",function(self)self:SetBackdropBorderColor(active and P().gold[1] or .46,active and P().gold[2] or .28,active and P().gold[3] or .13,on and 1 or .82) end)
            row:SetScript("OnClick",function()selectedDeckID=dRef.id;local ok,msg=HS.SetActiveDeck(dRef.id);if not ok then ShowStatus(msg or "Could not activate that deck.") else ShowStatus("Active deck: "..tostring(dRef.name or "Deck")) end;RPAL.RefreshHearthstone()end)
            dy=dy+49
        end
    end
    deckChild:SetHeight(math.max(1,dy))
    if C_Timer and C_Timer.After then local wanted=math.max(0,selectedOffset-45);C_Timer.After(0,function()if deckScroll and deckScroll.SetVerticalScroll then deckScroll:SetVerticalScroll(wanted) end end) elseif deckScroll.SetVerticalScroll then deckScroll:SetVerticalScroll(math.max(0,selectedOffset-45)) end

    editor=Dynamic(CreateFrame("Frame",nil,decks,"BackdropTemplate"));editor:SetPoint("TOPLEFT",8,-228);editor:SetPoint("BOTTOMRIGHT",-8,8);editor:SetBackdrop({bgFile=HS_PARCHMENT,tile=true,tileSize=96,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=7,insets={left=3,right=3,top=3,bottom=3}});editor:SetBackdropColor(.095,.045,.024,.97);editor:SetBackdropBorderColor(.64,.40,.18,.92);DressMenuPanel(editor,P().gold,.030)
    if selected then
        local title=DText(editor,Short(selected.name,24),13.5,P().gold,DISPLAY);title:SetPoint("TOPLEFT",12,-11);title:SetWidth(170);title:SetJustifyH("LEFT")
        local count=DText(editor,tostring(#(selected.cards or {})).." / 20",10,#(selected.cards or {})==20 and {.42,1,.56,1} or P().muted);count:SetPoint("TOPRIGHT",-12,-14)
        local active=DBtn(editor,selected.id==state.activeDeck and "ACTIVE DECK" or "USE THIS DECK",112,24,selected.id==state.activeDeck);active:SetPoint("TOPLEFT",10,-38);active:SetScript("OnClick",function()local ok,msg=HS.SetActiveDeck(selected.id);if not ok then ShowStatus(msg or "Could not activate that deck.") else selectedDeckID=selected.id;ShowStatus("Active deck: "..tostring(selected.name or "Deck")) end;RPAL.RefreshHearthstone()end)
        local rename=DBtn(editor,"Rename",58,24,false);rename:SetPoint("LEFT",active,"RIGHT",4,0);rename:SetScript("OnClick",function()Prompt("Rename Deck",selected.name,function(v)HS.RenameDeck(selected.id,v);RPAL.RefreshHearthstone()end)end)
        local del=DBtn(editor,"Delete",52,24,false);del:SetPoint("LEFT",rename,"RIGHT",4,0);del:SetScript("OnClick",function()local ok,err=HS.DeleteDeck(selected.id);if not ok then ShowStatus(err) end;selectedDeckID=state.activeDeck;RPAL.RefreshHearthstone()end)
        local fill=DBtn(editor,"Quick Fill",92,24,false);fill:SetPoint("TOPLEFT",10,-68);fill:SetScript("OnClick",function()local ok,err=HS.AutoFillDeck(selected.id);if not ok then ShowStatus(err) else ShowStatus("Filled deck to "..tostring(#selected.cards).." / 20.") end;RPAL.RefreshHearthstone()end)
        local clear=DBtn(editor,"Clear",62,24,false);clear:SetPoint("LEFT",fill,"RIGHT",5,0);clear:SetScript("OnClick",function()HS.ClearDeck(selected.id);RPAL.RefreshHearthstone()end)
        local rule=DText(editor,"Click or drag from Collection to add  •  drag a deck row back out to remove",7.8,P().muted);rule:SetPoint("TOPLEFT",11,-99);rule:SetWidth(318);rule:SetWordWrap(true)
        local list=Dynamic(CreateFrame("ScrollFrame",nil,editor,"UIPanelScrollFrameTemplate"));list:SetPoint("TOPLEFT",7,-119);list:SetPoint("BOTTOMRIGHT",-25,8)
        if RPAL.SkinScrollFrame then RPAL.SkinScrollFrame(list) end
        local lc=Dynamic(CreateFrame("Frame",nil,list));lc:SetSize(296,1);list:SetScrollChild(lc)
        local groups={};for _,cid in ipairs(selected.cards or {}) do groups[cid]=(groups[cid] or 0)+1 end
        local ids={};for cid in pairs(groups) do ids[#ids+1]=cid end
        table.sort(ids,function(a,b)local ca,cb=HS.GetCard(a),HS.GetCard(b);if not ca then return false elseif not cb then return true end;if (ca.cost or 0)==(cb.cost or 0) then return tostring(ca.name)<tostring(cb.name) end;return (ca.cost or 0)<(cb.cost or 0) end)
        for i,cid in ipairs(ids) do
            local cidRef=cid;local card=HS.GetCard(cidRef);local cardRef=card
            if card then
                local row=Dynamic(CreateFrame("Button",nil,lc,"BackdropTemplate"));row:SetSize(288,31);row:SetPoint("TOPLEFT",0,-(i-1)*34);row:SetBackdrop({bgFile=HS_LEATHER,tile=true,tileSize=64,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=6,insets={left=2,right=2,top=2,bottom=2}});row:SetBackdropColor(.21,.105,.045,.97);row:SetBackdropBorderColor(.47,.28,.12,.78)
                local gem=MakeManaGem(row,card.cost,24);gem:SetPoint("LEFT",3,0)
                local nm=DText(row,Short(card.name,24),9,P().pale,DISPLAY);nm:SetPoint("LEFT",gem,"RIGHT",6,0);nm:SetWidth(194);nm:SetJustifyH("LEFT");local copies=DText(row,"×"..tostring(groups[cid]),9.5,CardColor(card),DISPLAY);copies:SetPoint("RIGHT",-8,0)
                row:SetScript("OnEnter",function(self)self:SetBackdropBorderColor(P().gold[1],P().gold[2],P().gold[3],1);if ShowCardInspect then ShowCardInspect(cardRef,self) end end);row:SetScript("OnLeave",function(self)self:SetBackdropBorderColor(.47,.28,.12,.78);if HideCardInspect then HideCardInspect(self) end end)
                row:RegisterForDrag("LeftButton")
                row:SetScript("OnClick",function()for idx=#selected.cards,1,-1 do if selected.cards[idx]==cidRef then HS.RemoveCardFromDeck(selected.id,idx);break end end;RPAL.RefreshHearthstone()end)
                row:SetScript("OnDragStart",function(self)self._hsDragging=true;self:SetAlpha(.55);StartCollectionDragGhost(cardRef,"remove") end)
                row:SetScript("OnDragStop",function(self)
                    if not self._hsDragging then return end;self._hsDragging=nil;self:SetAlpha(1);StopCollectionDragGhost()
                    if not MouseOverFrame(editor) then
                        for idx=#selected.cards,1,-1 do if selected.cards[idx]==cidRef then HS.RemoveCardFromDeck(selected.id,idx);break end end
                        RPAL.RefreshHearthstone()
                    end
                end)
            end
        end
        lc:SetHeight(math.max(1,#ids*34))
    else
        local empty=DText(editor,"No deck selected",11,P().muted,DISPLAY);empty:SetPoint("CENTER",0,12);local sub=DText(editor,"Choose a deck above, or create a new one.",7.8,P().muted);sub:SetPoint("TOP",empty,"BOTTOM",0,-8)
    end
end

local function RenderPacks(content)
    local state=HS.GetState()
    local h=DText(content,"CARD PACKS",18,P().gold,DISPLAY);h:SetPoint("TOPLEFT",8,-8)
    local sub=DText(content,"Five cards per pack · at least one Rare or better · duplicates automatically return "..tostring(HS.GetCurrencyName and HS.GetCurrencyName() or "Tokens")..".",9,P().muted);sub:SetPoint("TOPLEFT",10,-38)
    local panel=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));panel:SetSize(520,300);panel:SetPoint("TOPLEFT",8,-74);Backdrop(panel,P().gold,.98);DressMenuPanel(panel,P().gold,.065)
    local packStage=CreateFrame("Frame",nil,panel,"BackdropTemplate");packStage:SetSize(136,158);packStage:SetPoint("TOP",0,-18);packStage:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=8,insets={left=2,right=2,top=2,bottom=2}});packStage:SetBackdropColor(.065,.025,.015,.99);packStage:SetBackdropBorderColor(1,.66,.22,.92)
    local packGlow=packStage:CreateTexture(nil,"BACKGROUND");packGlow:SetTexture(CELESTIAL.."starlight_glow.tga");packGlow:SetBlendMode("ADD");packGlow:SetPoint("TOPLEFT",-40,40);packGlow:SetPoint("BOTTOMRIGHT",40,-40);packGlow:SetVertexColor(1,.54,.16,.36)
    local packBack=MakeCardBack(packStage,(state and state.activeCardBack) or "classic",84,120,false);packBack:SetPoint("CENTER",0,0)
    local ribbon=packStage:CreateTexture(nil,"OVERLAY");ribbon:SetTexture(HS_PARCHMENT);ribbon:SetSize(120,28);ribbon:SetPoint("CENTER",0,-2);ribbon:SetVertexColor(.82,.50,.18,1)
    local seal=packStage:CreateTexture(nil,"OVERLAY");seal:SetTexture(ORNAMENT_SEAL);seal:SetSize(44,44);seal:SetPoint("CENTER",0,-2);seal:SetVertexColor(1,.78,.34,.96)
    local title=DText(panel,"TAVERN CARD PACK",18,P().gold,DISPLAY);title:SetPoint("TOP",0,-184)
    local owned=math.max(0,math.floor(tonumber(HS.GetOwnedPackCount and HS.GetOwnedPackCount() or 0) or 0))
    local ownedText=DText(panel,tostring(owned).." PACK"..(owned==1 and "" or "S").." OWNED",13,owned>0 and P().gold or P().pale,DISPLAY);ownedText:SetPoint("TOP",0,-210)
    local packHint=DText(panel,"Owned packs open directly. New packs cost "..tostring(HS.PACK_COST).." "..tostring(HS.GetCurrencyName and HS.GetCurrencyName() or "Tokens")..".",7.4,P().muted);packHint:SetPoint("TOP",ownedText,"BOTTOM",0,-5)
    local buy=DBtn(panel,"Buy Pack · "..tostring(HS.PACK_COST),146,38,false);buy:SetPoint("BOTTOMLEFT",105,24);buy:SetScript("OnClick",function()local ok,res=HS.BuyPack();if not ok then ShowStatus(res);return end;ShowStatus("Pack added to your collection.");RPAL.RefreshHearthstone()end)
    local open=DBtn(panel,owned>0 and "Open Pack" or "No Packs Owned",146,38,true);open:SetPoint("BOTTOMRIGHT",-105,24);open:SetScript("OnClick",function()local ok,res=HS.OpenPack();if not ok then ShowStatus(res);return end;RPAL.OpenHearthstonePackReveal(res);RPAL.RefreshHearthstone()end);if owned<=0 then open:Disable() end
    local odds=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));odds:SetSize(520,300);odds:SetPoint("TOPRIGHT",-8,-74);Backdrop(odds,P().border,.98);DressMenuPanel(odds,P().accent,.040)
    local oh=DText(odds,"RARITY ODDS",13,P().gold,DISPLAY);oh:SetPoint("TOPLEFT",18,-18)
    local rows={{"Common","70% per standard slot"},{"Rare","22% per standard slot"},{"Epic","6% per standard slot"},{"Legendary","2% per standard slot"}}
    for i,r in ipairs(rows) do local c=HS.GetCardRarityColor(r[1]);local t=DText(odds,r[1],12,c,DISPLAY);t:SetPoint("TOPLEFT",22,-56-(i-1)*43);local v=DText(odds,r[2],9,P().muted);v:SetPoint("TOPLEFT",160,-58-(i-1)*43) end
    local guarantee=DText(odds,"The fifth card is guaranteed Rare, Epic or Legendary.",8.5,P().pale);guarantee:SetPoint("BOTTOMLEFT",20,24);guarantee:SetWidth(470)
end

local function RenderDecks(content)
    local state=HS.GetState();selectedDeckID=selectedDeckID or state.activeDeck;local deck=state.decks[selectedDeckID] or HS.GetActiveDeck();selectedDeckID=deck and deck.id
    local left=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));left:SetPoint("TOPLEFT",6,-6);left:SetPoint("BOTTOMLEFT",6,6);left:SetWidth(290);Backdrop(left,P().border,.98)
    local lh=DText(left,"DECKS",14,P().gold,DISPLAY);lh:SetPoint("TOPLEFT",14,-14)
    local new=DBtn(left,"+ New Deck",116,28,false);new:SetPoint("TOPRIGHT",-12,-10);new:SetScript("OnClick",function()Prompt("New Deck","New Deck",function(v)local d=HS.CreateDeck(v);selectedDeckID=d.id;RPAL.RefreshHearthstone()end)end)
    local y=-52;for _,id in ipairs(state.deckOrder) do local d=state.decks[id];if d then local idRef=id;local dRef=d;local b=DBtn(left,Short(dRef.name,24),252,34,idRef==state.activeDeck);b:SetPoint("TOPLEFT",14,y);b:SetScript("OnClick",function()selectedDeckID=idRef;local ok,msg=HS.SetActiveDeck(idRef);if not ok then ShowStatus(msg or "Could not activate that deck.") else ShowStatus("Active deck: "..tostring(dRef.name or "Deck")) end;RPAL.RefreshHearthstone()end);y=y-40 end end
    local center=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));center:SetPoint("TOPLEFT",left,"TOPRIGHT",10,0);center:SetPoint("BOTTOMLEFT",left,"BOTTOMRIGHT",10,0);center:SetWidth(360);Backdrop(center,P().border,.98)
    if deck then
        local title=DText(center,deck.name,15,P().gold,DISPLAY);title:SetPoint("TOPLEFT",14,-14);local cnt=DText(center,tostring(#deck.cards).." / 20",10,#deck.cards==20 and {0.4,1,.55} or P().muted);cnt:SetPoint("TOPRIGHT",-14,-18)
        local active=DBtn(center,deck.id==state.activeDeck and "ACTIVE DECK" or "USE THIS DECK",112,26,deck.id==state.activeDeck);active:SetPoint("TOPLEFT",14,-43);active:SetScript("OnClick",function()local ok,msg=HS.SetActiveDeck(deck.id);if not ok then ShowStatus(msg or "Could not activate that deck.") else selectedDeckID=deck.id;ShowStatus("Active deck: "..tostring(deck.name or "Deck")) end;RPAL.RefreshHearthstone()end)
        local rename=DBtn(center,"Rename",78,26,false);rename:SetPoint("LEFT",active,"RIGHT",7,0);rename:SetScript("OnClick",function()Prompt("Rename Deck",deck.name,function(v)HS.RenameDeck(deck.id,v);RPAL.RefreshHearthstone()end)end)
        local del=DBtn(center,"Delete",72,26,false);del:SetPoint("LEFT",rename,"RIGHT",7,0);del:SetScript("OnClick",function()local ok,err=HS.DeleteDeck(deck.id);if not ok then ShowStatus(err) end;selectedDeckID=nil;RPAL.RefreshHearthstone()end)
        local scroll=Dynamic(CreateFrame("ScrollFrame",nil,center,"UIPanelScrollFrameTemplate"));scroll:SetPoint("TOPLEFT",10,-80);scroll:SetPoint("BOTTOMRIGHT",-30,10);local child=Dynamic(CreateFrame("Frame",nil,scroll));child:SetSize(310,1);scroll:SetScrollChild(child)
        if RPAL.SkinScrollFrame then RPAL.SkinScrollFrame(scroll) end
        for i,id in ipairs(deck.cards) do local card=HS.GetCard(id);local cardRef=card;local indexRef=i;local row=DBtn(child,cardRef and CardLabel(cardRef) or "Unknown",292,28,false);row:SetPoint("TOPLEFT",0,-(i-1)*31);if cardRef then local rc=CardColor(cardRef);row.label:SetTextColor(rc[1],rc[2],rc[3]);row:SetScript("OnEnter",function(self)if ShowCardInspect then ShowCardInspect(cardRef,self) end end);row:SetScript("OnLeave",function(self)if HideCardInspect then HideCardInspect(self) end end) end;row:SetScript("OnClick",function()HS.RemoveCardFromDeck(deck.id,indexRef);RPAL.RefreshHearthstone()end) end;child:SetHeight(math.max(1,#deck.cards*31))
    end
    local right=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));right:SetPoint("TOPLEFT",center,"TOPRIGHT",10,0);right:SetPoint("BOTTOMRIGHT",-6,6);Backdrop(right,P().border,.98)
    local rh=DText(right,"ADD FROM COLLECTION",12,P().gold,DISPLAY);rh:SetPoint("TOPLEFT",12,-14)
    local scroll=Dynamic(CreateFrame("ScrollFrame",nil,right,"UIPanelScrollFrameTemplate"));scroll:SetPoint("TOPLEFT",8,-44);scroll:SetPoint("BOTTOMRIGHT",-28,8);local child=Dynamic(CreateFrame("Frame",nil,scroll));child:SetSize(370,1);scroll:SetScrollChild(child)
    if RPAL.SkinScrollFrame then RPAL.SkinScrollFrame(scroll) end
    local idx=0;for _,card in ipairs(HS.CARDS) do local cardRef=card;local n=HS.GetCollectionCount(cardRef.id);if n>0 then idx=idx+1;local b=DBtn(child,CardLabel(cardRef).."   ×"..n,356,29,false);b:SetPoint("TOPLEFT",0,-(idx-1)*32);local rc=CardColor(cardRef);b.label:SetTextColor(rc[1],rc[2],rc[3]);b:SetScript("OnEnter",function(self)if ShowCardInspect then ShowCardInspect(cardRef,self) end end);b:SetScript("OnLeave",function(self)if HideCardInspect then HideCardInspect(self) end end);b:SetScript("OnClick",function()if deck then local ok,err=HS.AddCardToDeck(deck.id,cardRef.id);if not ok then ShowStatus(err) end;RPAL.RefreshHearthstone()end end) end end;child:SetHeight(math.max(1,idx*32))
end

local function RenderCosmetics(content)
    local state=HS.GetState()
    local h=DText(content,"CUSTOMIZATION",18,P().gold,DISPLAY);h:SetPoint("TOPLEFT",8,-8)
    local sub=DText(content,"Unlock arenas and card backs with "..tostring(HS.GetCurrencyName and HS.GetCurrencyName() or "Tokens")..", then equip the look you want to bring to the table.",9.5,P().muted);sub:SetPoint("TOPLEFT",10,-38);sub:SetWidth(1060)

    local function PageControls(parent,page,count,pageSize)
        local pages=math.max(1,math.ceil(count/pageSize));page=math.max(1,math.min(pages,page))
        local label=DText(parent,"PAGE  "..page.." / "..pages,8.4,P().muted,DISPLAY);label:SetPoint("BOTTOM",0,9)
        local prev=DBtn(parent,"‹",30,24,false);prev:SetPoint("RIGHT",label,"LEFT",-10,0);prev:SetEnabled(page>1)
        local nextb=DBtn(parent,"›",30,24,false);nextb:SetPoint("LEFT",label,"RIGHT",10,0);nextb:SetEnabled(page<pages)
        return prev,nextb,pages,page
    end

    local arenas=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));arenas:SetPoint("TOPLEFT",8,-72);arenas:SetSize(1090,220);Backdrop(arenas,P().border,.98);DressMenuPanel(arenas,P().gold,.028)
    local ah=DText(arenas,"ARENAS",13,P().gold,DISPLAY);ah:SetPoint("TOPLEFT",16,-14)
    local arenaPageSize=4;local aprev,anext,apages,apage=PageControls(arenas,cosmeticArenaPage,#(HS.ARENAS or {}),arenaPageSize);cosmeticArenaPage=apage
    aprev:SetScript("OnClick",function()cosmeticArenaPage=math.max(1,cosmeticArenaPage-1);RPAL.RefreshHearthstone()end);anext:SetScript("OnClick",function()cosmeticArenaPage=math.min(apages,cosmeticArenaPage+1);RPAL.RefreshHearthstone()end)
    local first=(apage-1)*arenaPageSize+1
    for slot=1,arenaPageSize do
        local def=HS.ARENAS[first+slot-1]
        if def then
            local x=16+(slot-1)*266
            local card=Dynamic(CreateFrame("Button",nil,arenas,"BackdropTemplate"));card:SetSize(248,142);card:SetPoint("TOPLEFT",x,-42)
            card:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=8,insets={left=2,right=2,top=2,bottom=2}});card:SetBackdropColor(.055,.028,.022,.995);card:SetBackdropBorderColor(def.accent[1],def.accent[2],def.accent[3],state.activeArena==def.id and 1 or .48)
            local preview=card:CreateTexture(nil,"BACKGROUND");preview:SetTexture(HS_WOOD);preview:SetPoint("TOPLEFT",7,-7);preview:SetPoint("BOTTOMRIGHT",-7,30);preview:SetVertexColor(.30,.18,.10,1)
            local topHalf=card:CreateTexture(nil,"BACKGROUND",nil,1);topHalf:SetTexture(WHITE);topHalf:SetPoint("TOPLEFT",7,-7);topHalf:SetPoint("BOTTOMRIGHT",card,"RIGHT",-7,12);topHalf:SetColorTexture(def.field[1],def.field[2],def.field[3],.40)
            local motif=card:CreateTexture(nil,"BORDER");motif:SetTexture(ArenaPattern(def));motif:SetPoint("TOPLEFT",8,-8);motif:SetPoint("BOTTOMRIGHT",-8,31);motif:SetVertexColor(def.accent[1],def.accent[2],def.accent[3],.25);motif:SetAlpha(.14)
            local rail=card:CreateTexture(nil,"ARTWORK");rail:SetTexture(WHITE);rail:SetPoint("TOPLEFT",7,-7);rail:SetPoint("TOPRIGHT",-7,-7);rail:SetHeight(24);rail:SetColorTexture(def.rail[1],def.rail[2],def.rail[3],1)
            local lip=card:CreateTexture(nil,"ARTWORK",nil,1);lip:SetTexture(WHITE);lip:SetPoint("TOPLEFT",12,-31);lip:SetPoint("TOPRIGHT",-12,-31);lip:SetHeight(2);lip:SetColorTexture(def.accent[1],def.accent[2],def.accent[3],.90)
            local crest=card:CreateTexture(nil,"OVERLAY");crest:SetTexture(OrnamentForArena(def,true));crest:SetSize(72,72);crest:SetPoint("LEFT",12,5);crest:SetVertexColor(def.accent[1],def.accent[2],def.accent[3],.82)
            local nm=CardText(card,def.name,10.2,P().pale,FONT);nm:SetPoint("TOPLEFT",88,-50);nm:SetWidth(145);nm:SetJustifyH("LEFT")
            local unlocked=state.unlockedArenas and state.unlockedArenas[def.id]
            local statusText=state.activeArena==def.id and "EQUIPPED" or (unlocked and "CLICK TO EQUIP" or ("BUY  ·  "..tostring(def.price or 0).." "..string.upper(HS.GetCurrencyName and HS.GetCurrencyName() or "Tokens")))
            local status=CardText(card,statusText,7.4,state.activeArena==def.id and {0.45,1,.58,1} or (unlocked and P().gold or P().muted),FONT);status:SetPoint("BOTTOMLEFT",88,15)
            card:SetScript("OnEnter",function(self)self:SetBackdropBorderColor(def.accent[1],def.accent[2],def.accent[3],1);self:SetScale(1.018)end)
            card:SetScript("OnLeave",function(self)self:SetScale(1);self:SetBackdropBorderColor(def.accent[1],def.accent[2],def.accent[3],state.activeArena==def.id and 1 or .48)end)
            card:SetScript("OnClick",function()local ok,msg;if unlocked then ok,msg=HS.SetActiveArena(def.id) else ok,msg=HS.UnlockArena(def.id) end;if not ok and msg then ShowStatus(msg) elseif msg then ShowStatus(msg) end;RPAL.RefreshHearthstone()end)
        end
    end

    local backs=Dynamic(CreateFrame("Frame",nil,content,"BackdropTemplate"));backs:SetPoint("TOPLEFT",8,-302);backs:SetPoint("BOTTOMRIGHT",-8,8);Backdrop(backs,P().border,.98);DressMenuPanel(backs,P().accent,.026)
    local bh=DText(backs,"CARD BACKS",13,P().gold,DISPLAY);bh:SetPoint("TOPLEFT",16,-14)
    local hint=DText(backs,"Large previews, page by page. Locked designs are purchased with "..tostring(HS.GetCurrencyName and HS.GetCurrencyName() or "Tokens").."; owned designs equip instantly.",8.2,P().muted);hint:SetPoint("TOPLEFT",16,-34);hint:SetWidth(760)
    local backPageSize=3;local bprev,bnext,bpages,bpage=PageControls(backs,cosmeticBackPage,#(HS.CARD_BACKS or {}),backPageSize);cosmeticBackPage=bpage
    bprev:SetScript("OnClick",function()cosmeticBackPage=math.max(1,cosmeticBackPage-1);RPAL.RefreshHearthstone()end);bnext:SetScript("OnClick",function()cosmeticBackPage=math.min(bpages,cosmeticBackPage+1);RPAL.RefreshHearthstone()end)
    local bfirst=(bpage-1)*backPageSize+1
    for slot=1,backPageSize do
        local def=HS.CARD_BACKS[bfirst+slot-1]
        if def then
            local col=(slot-1)%3;local x=14+col*354;local y=-58
            local host=Dynamic(CreateFrame("Button",nil,backs,"BackdropTemplate"));host:SetSize(338,192);host:SetPoint("TOPLEFT",x,y)
            host:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=8,insets={left=2,right=2,top=2,bottom=2}});host:SetBackdropColor(.052,.028,.022,.98);host:SetBackdropBorderColor(def.edge[1],def.edge[2],def.edge[3],state.activeCardBack==def.id and 1 or .48)
            local cb=MakeCardBack(host,def.id,96,136,false);cb:SetPoint("LEFT",14,0)
            local nm=CardText(host,def.name,10.2,P().pale,FONT);nm:SetPoint("TOPLEFT",126,-42);nm:SetWidth(196);nm:SetJustifyH("LEFT")
            local unlocked=state.unlockedCardBacks and state.unlockedCardBacks[def.id]
            local label=state.activeCardBack==def.id and "EQUIPPED" or (unlocked and "CLICK TO EQUIP" or ("BUY  ·  "..tostring(def.price or 0).." "..string.upper(HS.GetCurrencyName and HS.GetCurrencyName() or "Tokens")))
            local status=CardText(host,label,7.6,state.activeCardBack==def.id and {0.45,1,.58,1} or (unlocked and P().gold or P().muted),FONT);status:SetPoint("TOPLEFT",126,-72)
            local desc=CardText(host,"Card back cosmetic",7.2,P().muted,FONT);desc:SetPoint("TOPLEFT",126,-101)
            host:SetScript("OnEnter",function(self)self:SetScale(1.018);self:SetBackdropBorderColor(def.edge[1],def.edge[2],def.edge[3],1)end)
            host:SetScript("OnLeave",function(self)self:SetScale(1);self:SetBackdropBorderColor(def.edge[1],def.edge[2],def.edge[3],state.activeCardBack==def.id and 1 or .48)end)
            host:SetScript("OnClick",function()local ok,msg;if unlocked then ok,msg=HS.SetActiveCardBack(def.id) else ok,msg=HS.UnlockCardBack(def.id) end;if not ok and msg then ShowStatus(msg) elseif msg then ShowStatus(msg) end;RPAL.RefreshHearthstone()end)
        end
    end
end


local function RenderHistory(content)
    local h=DText(content,"MATCH HISTORY",17,P().gold,DISPLAY);h:SetPoint("TOPLEFT",8,-8)
    local hist=HS.GetHistory()
    if #hist==0 then local e=DText(content,"No matches recorded yet.",10,P().muted);e:SetPoint("TOPLEFT",10,-48);return end
    local scroll=Dynamic(CreateFrame("ScrollFrame",nil,content,"UIPanelScrollFrameTemplate"));scroll:SetPoint("TOPLEFT",8,-46);scroll:SetPoint("BOTTOMRIGHT",-28,8)
    if RPAL.SkinScrollFrame then RPAL.SkinScrollFrame(scroll) end
    local child=Dynamic(CreateFrame("Frame",nil,scroll));child:SetSize(1125,1);scroll:SetScrollChild(child)
    for i,item in ipairs(hist) do
        local row=Dynamic(CreateFrame("Frame",nil,child,"BackdropTemplate"));row:SetSize(1110,48);row:SetPoint("TOPLEFT",0,-(i-1)*55)
        Backdrop(row,item.result=="Win" and {0.22,.7,.35} or (item.result=="Loss" and {.8,.25,.25} or P().border),.98)
        local r=DText(row,item.result,12,item.result=="Win" and {.4,1,.55} or (item.result=="Loss" and {1,.45,.45} or P().pale),DISPLAY);r:SetPoint("LEFT",14,0)
        local o=DText(row,"vs. "..tostring(item.opponent or "Unknown"),10,P().pale);o:SetPoint("LEFT",90,0);o:SetWidth(610);o:SetJustifyH("LEFT")
        local m=DText(row,tostring(item.turns or 0).." turns · "..tostring(item.reason or "match"),8.5,P().muted);m:SetPoint("RIGHT",-14,0);m:SetWidth(330);m:SetJustifyH("RIGHT")
    end
    child:SetHeight(math.max(1,#hist*55))
end

local function AnimateMainContent(f)
    if not f or not f.content then return end
    if f._tabFade and f._tabFade:IsPlaying() then f._tabFade:Stop() end
    f.content:SetAlpha(0)
    local g=f.content:CreateAnimationGroup();f._tabFade=g
    local a=g:CreateAnimation("Alpha");a:SetOrder(1);a:SetFromAlpha(0);a:SetToAlpha(1);a:SetDuration(.18)
    g:SetScript("OnFinished",function()if f and f.content then f.content:SetAlpha(1) end end);g:Play()
end

RPAL.OnHSTokensChanged=function()
    if RPAL.RefreshHearthstone then pcall(RPAL.RefreshHearthstone) end
end
RPAL.OnHSOwnedPacksChanged=function()
    if RPAL.RefreshHearthstone then pcall(RPAL.RefreshHearthstone) end
end

function RPAL.RefreshHearthstone()
    if HideCardInspect then HideCardInspect() end
    if selectedTab=="decks" then selectedTab="collection" end
    local f=EnsureMain();local changed=f._lastTab~=selectedTab;f.tokens:SetText(string.upper(HS.GetCurrencyName and HS.GetCurrencyName() or "Tokens").."  "..tostring(HS.GetCurrencyBalance and HS.GetCurrencyBalance() or 0));RefreshTabVisuals();Clear(f.content)
    if selectedTab=="play" then RenderPlay(f.content) elseif selectedTab=="collection" then RenderCollection(f.content) elseif selectedTab=="packs" then RenderPacks(f.content) elseif selectedTab=="cosmetics" then RenderCosmetics(f.content) else RenderHistory(f.content) end
    if changed then AnimateMainContent(f) end;f._lastTab=selectedTab
end
function RPAL.OpenHearthstone(tab)
    local f=EnsureMain();if tab then selectedTab=(tab=="decks" and "collection" or tab) end
    if RPAL.UI and RPAL.UI.main then RPAL.UI.main:Hide() end;RPAL.RefreshHearthstone();if RPAL.PrepareWindow then RPAL.PrepareWindow(f,nil,0.15)else f:Show();f:Raise()end;return f
end

function RPAL.OpenHearthstoneAbilityPicker()
    local sh=CreateFrame("Frame",nil,UIParent,"BackdropTemplate");sh:SetAllPoints();sh:SetFrameStrata("TOOLTIP");sh:SetFrameLevel(9300);sh:EnableMouse(true);sh:SetBackdrop({bgFile=WHITE});sh:SetBackdropColor(0,0,0,.62)
    local p=CreateFrame("Frame",nil,sh,"BackdropTemplate");p:SetSize(820,590);p:SetPoint("CENTER");Backdrop(p,P().gold,.995);p:SetBackdropColor(.085,.038,.020,.998);DressMenuPanel(p,P().gold,.16)
    local h=Text(p,"CHOOSE YOUR HERO ABILITY",22,P().gold,DISPLAY);h:SetPoint("TOP",0,-20)
    local intro=Text(p,"Pick a playstyle. Each ability can be used once per turn while you can pay its Mana cost.",9.5,P().muted,FONT);intro:SetPoint("TOP",h,"BOTTOM",0,-9);intro:SetWidth(700);intro:SetJustifyH("CENTER")
    local active=HS.GetState().activeAbility
    for i,a in ipairs(HS.HERO_ABILITIES) do
        local row=math.floor((i-1)/2);local col=(i-1)%2
        local b=CreateFrame("Button",nil,p,"BackdropTemplate");b:SetSize(370,100);b:SetPoint("TOPLEFT",28+col*394,-94-row*111);b:EnableMouse(true)
        b:SetBackdrop({bgFile=HS_LEATHER,tile=true,tileSize=64,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=9,insets={left=3,right=3,top=3,bottom=3}})
        local selected=a.id==active
        b:SetBackdropColor(selected and .26 or .15,selected and .125 or .068,selected and .045 or .030,.995)
        local bc=selected and P().gold or P().border;b:SetBackdropBorderColor(bc[1],bc[2],bc[3],selected and 1 or .88)
        local iconBox=CreateFrame("Frame",nil,b,"BackdropTemplate");iconBox:SetSize(64,64);iconBox:SetPoint("LEFT",12,0);iconBox:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=7,insets={left=2,right=2,top=2,bottom=2}});iconBox:SetBackdropColor(.035,.018,.012,.99);iconBox:SetBackdropBorderColor(P().gold[1],P().gold[2],P().gold[3],.65);iconBox:EnableMouse(false)
        local icon=iconBox:CreateTexture(nil,"ARTWORK");icon:SetPoint("TOPLEFT",4,-4);icon:SetPoint("BOTTOMRIGHT",-4,4);icon:SetTexture(a.icon or "Interface\\Icons\\INV_Misc_QuestionMark");icon:SetTexCoord(.08,.92,.08,.92)
        local title=Text(b,a.name,13.2,P().pale,DISPLAY);title:SetPoint("TOPLEFT",88,-14);title:SetWidth(188);title:SetJustifyH("LEFT");title:SetWordWrap(false)
        local mana=CreateFrame("Frame",nil,b,"BackdropTemplate");mana:SetSize(70,24);mana:SetPoint("TOPRIGHT",-12,-11);mana:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=5,insets={left=1,right=1,top=1,bottom=1}});mana:SetBackdropColor(.035,.13,.31,.98);mana:SetBackdropBorderColor(.35,.70,1,.90);mana:EnableMouse(false)
        local manaText=Text(mana,tostring(a.cost or 0).." MANA",8.3,{.72,.88,1,1},FONT);manaText:SetPoint("CENTER")
        local desc=Text(b,a.text,9.4,P().muted,FONT);desc:SetPoint("TOPLEFT",88,-42);desc:SetWidth(260);desc:SetHeight(44);desc:SetJustifyH("LEFT");desc:SetJustifyV("TOP");desc:SetWordWrap(true)
        if selected then local sel=Text(b,"SELECTED",7.3,P().gold,DISPLAY);sel:SetPoint("BOTTOMRIGHT",-14,8) end
        b:SetScript("OnEnter",function(self)self:SetBackdropBorderColor(P().gold[1],P().gold[2],P().gold[3],1);self:SetBackdropColor(.27,.135,.055,1) end)
        b:SetScript("OnLeave",function(self)local current=a.id==HS.GetState().activeAbility;local c=current and P().gold or P().border;self:SetBackdropBorderColor(c[1],c[2],c[3],current and 1 or .88);self:SetBackdropColor(current and .26 or .15,current and .125 or .068,current and .045 or .030,.995) end)
        b:SetScript("OnClick",function()HS.SetActiveAbility(a.id);sh:Hide();RPAL.RefreshHearthstone()end)
    end
    local cancel=Btn(p,"Cancel",112,32,false);cancel:SetPoint("BOTTOM",0,18);cancel:SetScript("OnClick",function()sh:Hide()end)
    sh:EnableKeyboard(true);if sh.SetPropagateKeyboardInput then sh:SetPropagateKeyboardInput(true) end
    sh:SetScript("OnKeyDown",function(self,key)if key=="ESCAPE" then if self.SetPropagateKeyboardInput then self:SetPropagateKeyboardInput(false) end;self:Hide() else if self.SetPropagateKeyboardInput then self:SetPropagateKeyboardInput(true) end end end)
end

function RPAL.OpenHearthstoneStakePicker()
    local sh=CreateFrame("Frame",nil,UIParent,"BackdropTemplate");sh:SetAllPoints();sh:SetFrameStrata("TOOLTIP");sh:EnableMouse(true);sh:SetBackdrop({bgFile=WHITE});sh:SetBackdropColor(0,0,0,.55)
    local p=CreateFrame("Frame",nil,sh,"BackdropTemplate");p:SetSize(760,560);p:SetPoint("CENTER");Backdrop(p,P().gold,.995);local h=Text(p,"STAKE INDIVIDUAL CARDS",18,P().gold,DISPLAY);h:SetPoint("TOPLEFT",22,-18);local sub=Text(p,"Only selected cards are wagered. Decks are never staked wholesale.",9,P().muted);sub:SetPoint("TOPLEFT",24,-46)
    local scroll=CreateFrame("ScrollFrame",nil,p,"UIPanelScrollFrameTemplate");scroll:SetPoint("TOPLEFT",20,-78);scroll:SetPoint("BOTTOMRIGHT",-36,52);local child=CreateFrame("Frame",nil,scroll);child:SetSize(690,1);scroll:SetScrollChild(child)
    if RPAL.SkinScrollFrame then RPAL.SkinScrollFrame(scroll) end
    local state=HS.GetState();local y=0;for _,card in ipairs(HS.CARDS) do local count=HS.GetCollectionCount(card.id);if count>0 then
        local staked=HS.GetStakeCount(card.id);local row=CreateFrame("Frame",nil,child,"BackdropTemplate");row:SetSize(660,32);row:SetPoint("TOPLEFT",0,-y);Backdrop(row,staked>0 and CardColor(card) or P().border,.96)
        local rc=CardColor(card);local name=Text(row,card.name,9.5,rc,DISPLAY);name:SetPoint("LEFT",12,0);name:SetWidth(360);name:SetWordWrap(false)
        local countText=Text(row,"Owned "..count.."   ·   Staked "..staked,8.5,P().muted);countText:SetPoint("RIGHT",-92,0)
        local minus=Btn(row,"−",30,24,false);minus:SetPoint("RIGHT",-48,0);minus:SetEnabled(staked>0);minus:SetScript("OnClick",function()HS.AdjustStake(card.id,-1);sh:Hide();RPAL.OpenHearthstoneStakePicker();RPAL.RefreshHearthstone()end)
        local plus=Btn(row,"+",30,24,staked<count);plus:SetPoint("RIGHT",-12,0);plus:SetEnabled(staked<count);plus:SetScript("OnClick",function()local ok,err=HS.AdjustStake(card.id,1);if not ok and err then ShowStatus(err) end;sh:Hide();RPAL.OpenHearthstoneStakePicker();RPAL.RefreshHearthstone()end)
        y=y+36
    end end;child:SetHeight(math.max(1,y))
    local clear=Btn(p,"Clear Stakes",110,28,false);clear:SetPoint("BOTTOMLEFT",20,16);clear:SetScript("OnClick",function()HS.ClearStakes();sh:Hide();RPAL.RefreshHearthstone()end);local done=Btn(p,"Done",110,28,true);done:SetPoint("BOTTOMRIGHT",-20,16);done:SetScript("OnClick",function()sh:Hide();RPAL.RefreshHearthstone()end)
end

function RPAL.OpenHearthstonePackReveal(results)
    if packFrame then packFrame:Hide() end
    local state=HS.GetState();local backID=state.activeCardBack or "classic"
    local sh=CreateFrame("Frame",nil,UIParent,"BackdropTemplate");packFrame=sh;sh:SetAllPoints();sh:SetFrameStrata("TOOLTIP");sh:SetFrameLevel(9800);sh:EnableMouse(true);sh:SetBackdrop({bgFile=WHITE});sh:SetBackdropColor(.012,.008,.012,.968)
    if RPAL.RegisterEscapeFrame then RPAL.RegisterEscapeFrame(sh) end

    local halo=sh:CreateTexture(nil,"BACKGROUND");halo:SetTexture(CELESTIAL.."starlight_round.tga");halo:SetBlendMode("ADD");halo:SetSize(1180,850);halo:SetPoint("CENTER");halo:SetVertexColor(1,.50,.15,.10)
    local p=CreateFrame("Frame",nil,sh,"BackdropTemplate");p:SetSize(1160,720);p:SetPoint("CENTER");p:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=14,insets={left=4,right=4,top=4,bottom=4}});p:SetBackdropColor(.058,.030,.022,.995);p:SetBackdropBorderColor(P().gold[1],P().gold[2],P().gold[3],.95);p:EnableMouse(true)
    local floor=p:CreateTexture(nil,"BACKGROUND");floor:SetTexture(WHITE);floor:SetPoint("TOPLEFT",72,-94);floor:SetPoint("BOTTOMRIGHT",-72,76);floor:SetColorTexture(.145,.082,.050,.96)
    local floorPattern=p:CreateTexture(nil,"BACKGROUND",nil,1);floorPattern:SetTexture(ATMOSPHERE_ROOT.."atmosphere_candlelit.tga");floorPattern:SetPoint("TOPLEFT",74,-96);floorPattern:SetPoint("BOTTOMRIGHT",-74,78);floorPattern:SetVertexColor(1,.72,.42,.11)
    local h=Text(p,"SEALED CARD PACK",27,P().gold,TITLE_FONT);h:SetPoint("TOP",0,-24)
    local hint=Text(p,"Open the pack, then click each face-down card to reveal it.",9.5,P().muted);hint:SetPoint("TOP",0,-62)

    local emergencyClose=Btn(p,"×",34,28,false);emergencyClose:SetPoint("TOPRIGHT",-14,-14);emergencyClose.label:SetFont(TITLE_FONT,Scaled(18),"OUTLINE");emergencyClose:SetFrameLevel(p:GetFrameLevel()+180);emergencyClose:SetScript("OnClick",function()sh:Hide();if RPAL.RefreshHearthstone then RPAL.RefreshHearthstone() end end)

    local positions={{0,112},{-252,38},{252,38},{-164,-194},{164,-194}}
    local holders={};local revealed=0;local launched=false
    local openAnother,done

    local function RarityBurst(host,card)
        if not host or not card then return end
        local rc=CardColor(card)
        local ring=host:CreateTexture(nil,"OVERLAY",nil,6);ring:SetTexture(CELESTIAL.."starlight_round.tga");ring:SetBlendMode("ADD");ring:SetSize(215,215);ring:SetPoint("CENTER");ring:SetVertexColor(rc[1],rc[2],rc[3],.58)
        local g=ring:CreateAnimationGroup();local s=g:CreateAnimation("Scale");s:SetScale(1.42,1.42);s:SetDuration(.34);s:SetSmoothing("OUT");local a=g:CreateAnimation("Alpha");a:SetFromAlpha(.88);a:SetToAlpha(0);a:SetDuration(.36);g:SetScript("OnFinished",function()ring:Hide()end);g:Play()
        for i=1,8 do local spark=host:CreateTexture(nil,"OVERLAY",nil,7);spark:SetTexture(CELESTIAL..((i%2==0) and "starlight_pin.tga" or "starlight_diamond.tga"));spark:SetBlendMode("ADD");spark:SetSize(7+(i%3)*2,14+(i%3)*4);spark:SetPoint("CENTER");spark:SetVertexColor(rc[1],rc[2],rc[3],.90);local sg=spark:CreateAnimationGroup();local mv=sg:CreateAnimation("Translation");local ang=(i/8)*6.28318;mv:SetOffset(math.cos(ang)*(34+(i%3)*8),math.sin(ang)*(28+(i%2)*8));mv:SetDuration(.28);local fa=sg:CreateAnimation("Alpha");fa:SetFromAlpha(.92);fa:SetToAlpha(0);fa:SetDuration(.30);sg:SetScript("OnFinished",function()spark:Hide()end);sg:Play() end
    end

    local function SetHolderHover(holder,active)
        if not holder or not holder._ready or holder._flipping then return end
        holder._hovering=active and true or false
        holder:SetScale(active and 1.055 or 1)
        if holder.glow then holder.glow:SetAlpha(active and 1 or 0) end
        if holder.ring then holder.ring:SetAlpha(active and 1 or 0) end
        if active then
            if not holder.tiltDriver then holder.tiltDriver=CreateFrame("Frame",nil,holder) end
            holder.tiltDriver:Show();holder.tiltDriver:SetScript("OnUpdate",function()
                if not holder._hovering or not holder:IsShown() then return end
                local l,r,b,t=holder:GetLeft(),holder:GetRight(),holder:GetBottom(),holder:GetTop();if not l or not r or not b or not t then return end
                local x,y=GetCursorPosition();local scale=UIParent:GetEffectiveScale() or 1;x=x/scale;y=y/scale
                local dx=math.max(-1,math.min(1,(x-(l+r)*.5)/math.max(1,(r-l)*.5)));local dy=math.max(-1,math.min(1,(y-(b+t)*.5)/math.max(1,(t-b)*.5)))
                holder.visual:ClearAllPoints();holder.visual:SetPoint("CENTER",holder,"CENTER",dx*5,dy*3.5)
            end)
        else
            if holder.tiltDriver then holder.tiltDriver:SetScript("OnUpdate",nil);holder.tiltDriver:Hide() end
            holder.visual:ClearAllPoints();holder.visual:SetPoint("CENTER")
        end
    end

    local function FinishFlip(holder,res)
        if not holder or holder._revealed then return end
        holder._revealed=true;holder._flipping=nil;holder:EnableMouse(true);holder.visual:SetScale(1)
        if holder.backFace then holder.backFace:Hide() end
        if holder.faceCard then holder.faceCard:SetAlpha(1);holder.faceCard:Show();if holder.faceCard.Raise then holder.faceCard:Raise() end end
        if holder.dup then holder.dup:Show() end
        RarityBurst(holder,res.card)
        revealed=revealed+1
        if openAnother then openAnother:Show() end
        if revealed>=#(results or {}) then hint:SetText("Pack complete. Open another pack or return to the Packs menu.") else hint:SetText("Card revealed. You can keep revealing, or open another pack below.") end
    end

    local function Reveal(holder,res)
        if not launched or not holder or not holder._ready or holder._revealed or holder._flipping then return end
        holder._flipping=true;holder:EnableMouse(false);SetHolderHover(holder,false)
        if PlaySound and SOUNDKIT then pcall(PlaySound,(res.card.rarity=="Legendary" and (SOUNDKIT.UI_LEGENDARY_LOOT_TOAST or SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON)) or SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON) end
        local swapped=false
        local function SwapFace()
            if swapped then return end;swapped=true
            if holder.backFace then holder.backFace:Hide() end
            if holder.faceCard then holder.faceCard:SetAlpha(1);holder.faceCard:Show() end
            holder.visual:SetScale(.12)
            local expand=holder.visual:CreateAnimationGroup();local ex=expand:CreateAnimation("Scale");ex:SetScale(8.333,8.333);ex:SetDuration(.15);ex:SetSmoothing("OUT");expand:SetScript("OnFinished",function()FinishFlip(holder,res) end);expand:Play()
            if C_Timer and C_Timer.After then C_Timer.After(.19,function()FinishFlip(holder,res) end) end
        end
        local fold=holder.visual:CreateAnimationGroup();local sc=fold:CreateAnimation("Scale");sc:SetScale(.12,.92);sc:SetDuration(.13);sc:SetSmoothing("IN");fold:SetScript("OnFinished",SwapFace);fold:Play()
        if C_Timer and C_Timer.After then C_Timer.After(.16,SwapFace) end
    end

    for i,res in ipairs(results or {}) do
        local index=i;local result=res
        local holder=CreateFrame("Button",nil,p);holders[index]=holder;holder:SetSize(176,244);holder:SetPoint("CENTER",p,"CENTER",0,-8);holder:SetFrameLevel(p:GetFrameLevel()+30+index);holder:EnableMouse(false);holder:Hide();holder._finalX=positions[index][1];holder._finalY=positions[index][2]
        if holder.RegisterForClicks then holder:RegisterForClicks("LeftButtonUp") end
        local rc=CardColor(result.card)
        holder.glow=holder:CreateTexture(nil,"BACKGROUND",nil,3);holder.glow:SetTexture(CELESTIAL.."starlight_glow.tga");holder.glow:SetBlendMode("ADD");holder.glow:SetPoint("TOPLEFT",-44,44);holder.glow:SetPoint("BOTTOMRIGHT",44,-44);holder.glow:SetVertexColor(rc[1],rc[2],rc[3],.92);holder.glow:SetAlpha(0)
        holder.ring=CreateFrame("Frame",nil,holder,"BackdropTemplate");holder.ring:SetPoint("TOPLEFT",-10,10);holder.ring:SetPoint("BOTTOMRIGHT",10,-10);holder.ring:SetFrameLevel(holder:GetFrameLevel()+2);holder.ring:SetBackdrop({edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=15});holder.ring:SetBackdropBorderColor(rc[1],rc[2],rc[3],1);holder.ring:SetAlpha(0);holder.ring:EnableMouse(false)
        holder.visual=CreateFrame("Frame",nil,holder);holder.visual:SetSize(170,238);holder.visual:SetPoint("CENTER");holder.visual:SetFrameLevel(holder:GetFrameLevel()+5);holder.visual:EnableMouse(false)
        holder.backFace=MakeCardBack(holder.visual,backID,154,216,false);holder.backFace:SetPoint("CENTER");holder.backFace:SetFrameLevel(holder.visual:GetFrameLevel()+4)
        holder.faceCard=MakeCard(holder.visual,result.card,154,216,false,{noHover=true,static=true,noMouse=true});holder.faceCard:SetPoint("CENTER");holder.faceCard:SetFrameLevel(holder.visual:GetFrameLevel()+8);holder.faceCard:Hide()
        if result.duplicate then local dup=CreateFrame("Frame",nil,holder,"BackdropTemplate");holder.dup=dup;dup:SetSize(146,23);dup:SetPoint("BOTTOM",0,-8);dup:SetFrameLevel(holder:GetFrameLevel()+40);dup:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=7,insets={left=2,right=2,top=2,bottom=2}});dup:SetBackdropColor(.09,.055,.015,.98);dup:SetBackdropBorderColor(1,.66,.18,1);local dt=CardText(dup,"DUPLICATE  +"..tostring(result.refund).." "..string.upper(HS.GetCurrencyName and HS.GetCurrencyName() or "Tokens"),6.9,{1,.82,.34,1},FONT);dt:SetPoint("CENTER");dup:Hide() end
        holder:SetScript("OnEnter",function(self)SetHolderHover(self,true)end);holder:SetScript("OnLeave",function(self)SetHolderHover(self,false)end);holder:SetScript("OnClick",function(self)Reveal(self,result)end)
    end

    local function OpenAnotherPack()
        local ok,res=HS.OpenPack();if not ok then ShowStatus(res);return end
        sh:Hide();RPAL.OpenHearthstonePackReveal(res);if RPAL.RefreshHearthstone then RPAL.RefreshHearthstone() end
    end
    openAnother=Btn(p,"Open Another Pack",176,32,true);openAnother:SetPoint("BOTTOM",0,18);openAnother:Hide();openAnother:SetScript("OnClick",OpenAnotherPack)
    done=Btn(p,"Back to Packs",126,30,false);done:SetPoint("BOTTOMRIGHT",-28,20);done:SetScript("OnClick",function()sh:Hide();if RPAL.RefreshHearthstone then RPAL.RefreshHearthstone() end end)

    local pack=CreateFrame("Button",nil,p,"BackdropTemplate");pack:SetSize(226,300);pack:SetPoint("CENTER",0,-10);pack:SetFrameLevel(p:GetFrameLevel()+100);pack:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=12,insets={left=4,right=4,top=4,bottom=4}});pack:SetBackdropColor(.11,.045,.028,.995);pack:SetBackdropBorderColor(1,.70,.26,1)
    local packGlow=pack:CreateTexture(nil,"BACKGROUND");packGlow:SetTexture(CELESTIAL.."starlight_glow.tga");packGlow:SetBlendMode("ADD");packGlow:SetPoint("TOPLEFT",-60,60);packGlow:SetPoint("BOTTOMRIGHT",60,-60);packGlow:SetVertexColor(1,.48,.12,.58)
    local packBack=MakeCardBack(pack,backID,178,252,false);packBack:SetPoint("CENTER");packBack:SetFrameLevel(pack:GetFrameLevel()+2)
    local band=CreateFrame("Frame",nil,pack,"BackdropTemplate");band:SetSize(202,48);band:SetPoint("CENTER",0,-3);band:SetFrameLevel(pack:GetFrameLevel()+8);band:SetBackdrop({bgFile=HS_PARCHMENT,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=8,insets={left=3,right=3,top=3,bottom=3}});band:SetBackdropColor(.76,.48,.18,.99);band:SetBackdropBorderColor(1,.80,.38,1)
    local packText=Text(pack,"CLICK TO OPEN",11,{1,.88,.56,1},FONT);packText:SetPoint("BOTTOM",0,-28)
    pack:SetScript("OnEnter",function(self)self:SetScale(1.045);packGlow:SetAlpha(.88)end);pack:SetScript("OnLeave",function(self)self:SetScale(1);packGlow:SetAlpha(.58)end)

    local function FinalizeFlight(holder)
        if not holder or holder._ready then return end
        holder._ready=true;holder:ClearAllPoints();holder:SetPoint("CENTER",p,"CENTER",holder._finalX,holder._finalY);holder:SetAlpha(1);holder:SetScale(1);holder:Show();holder:EnableMouse(true)
    end
    local function LaunchCards()
        if launched then return end;launched=true;pack:Hide();h:SetText("CHOOSE YOUR REVEALS");hint:SetText("Click a face-down card to turn it over.")
        for i,holder in ipairs(holders) do
            holder:ClearAllPoints();holder:SetPoint("CENTER",p,"CENTER",0,-8);holder:SetAlpha(1);holder:SetScale(.86);holder:Show();holder:EnableMouse(false)
            local g=holder:CreateAnimationGroup();local mv=g:CreateAnimation("Translation");mv:SetOffset(holder._finalX,holder._finalY+8);mv:SetDuration(.30+(i-1)*.025);mv:SetSmoothing("OUT");local sc=g:CreateAnimation("Scale");sc:SetScale(1.16,1.16);sc:SetDuration(.30+(i-1)*.025);sc:SetSmoothing("OUT");g:SetScript("OnFinished",function()FinalizeFlight(holder)end);g:Play()
            if C_Timer and C_Timer.After then C_Timer.After(.42+(i*.02),function()FinalizeFlight(holder)end) end
        end
    end
    pack:SetScript("OnClick",function(self)if self._opened then return end;self._opened=true;self:EnableMouse(false);LaunchCards()end)
end

function RPAL.ShowHearthstoneResumeInviteChoice(target,record,dmMode)
    local sh=CreateFrame("Frame",nil,UIParent,"BackdropTemplate");sh:SetAllPoints();sh:SetFrameStrata("TOOLTIP");sh:EnableMouse(true);sh:SetBackdrop({bgFile=WHITE});sh:SetBackdropColor(0,0,0,.58)
    local p=CreateFrame("Frame",nil,sh,"BackdropTemplate");p:SetSize(560,300);p:SetPoint("CENTER");Backdrop(p,P().gold,.995)
    local h=Text(p,"PAUSED MATCH FOUND",20,P().gold,DISPLAY);h:SetPoint("TOP",0,-22)
    local who=Text(p,"You already have a paused match with "..Short(target,34)..".",11,P().pale);who:SetPoint("TOP",0,-62)
    local sub=Text(p,"Resume keeps the exact board, hands, Health, Mana and turn. The other player must approve the continuation.",10,P().muted);sub:SetPoint("TOPLEFT",34,-96);sub:SetPoint("TOPRIGHT",-34,-96);sub:SetJustifyH("CENTER");sub:SetWordWrap(true)
    local resume=Btn(p,"Resume Match",148,34,true);resume:SetPoint("BOTTOMRIGHT",-32,26);resume:SetScript("OnClick",function()sh:Hide();local ok,msg=HS.SendChallengeToTarget(target,dmMode,"resume");ShowStatus(msg or (ok and "Resume invitation sent." or "Could not send resume invitation."))end)
    local fresh=Btn(p,"Start New",128,34,false);fresh:SetPoint("RIGHT",resume,"LEFT",-10,0);fresh:SetScript("OnClick",function()sh:Hide();local ok,msg=HS.SendChallengeToTarget(target,dmMode,"new");ShowStatus(msg or (ok and "New challenge sent." or "Could not send challenge."))end)
    local cancel=Btn(p,"Cancel",92,34,false);cancel:SetPoint("RIGHT",fresh,"LEFT",-10,0);cancel:SetScript("OnClick",function()sh:Hide()end)
end

function RPAL.ShowHearthstoneChallenge(id,ch)
    if challengeFrame then challengeFrame:Hide() end
    local sh=CreateFrame("Frame",nil,UIParent,"BackdropTemplate");challengeFrame=sh;sh:SetAllPoints();sh:SetFrameStrata("TOOLTIP");sh:EnableMouse(true);sh:SetBackdrop({bgFile=WHITE});sh:SetBackdropColor(0,0,0,.58)
    local canResume=ch.localPausedMatchID and true or false
    local p=CreateFrame("Frame",nil,sh,"BackdropTemplate");p:SetSize(580,canResume and 370 or 330);p:SetPoint("CENTER");Backdrop(p,P().gold,.995);local h=Text(p,ch.mode=="dm" and "DM UNOFFICIAL HEARTHSTONE TABLE" or (canResume and "RESUME UNOFFICIAL HEARTHSTONE MATCH" or "UNOFFICIAL HEARTHSTONE CHALLENGE"),20,P().gold,DISPLAY);h:SetPoint("TOP",0,-20);local who=Text(p,tostring(ch.heroName or ch.sender).." invites you to play.",11,P().pale);who:SetPoint("TOP",0,-61)
    local deck=Text(p,"Their deck: "..tostring(ch.deckName or "Unknown"),9.5,P().muted);deck:SetPoint("TOPLEFT",28,-100);local st=Text(p,"Their stake: "..(#(ch.stakes or {})>0 and tostring(#ch.stakes).." card(s)" or "No cards"),9.5,P().muted);st:SetPoint("TOPLEFT",28,-124)
    local yours=Text(p,"You will use "..tostring((HS.GetActiveDeck() or {}).name or "your active deck").." and your currently selected stakes.",9.5,P().pale);yours:SetPoint("TOPLEFT",28,-164);yours:SetWidth(520);yours:SetWordWrap(true)
    if canResume then
        local resumeInfo=Text(p,"A matching paused table exists on both clients. Resume requires your approval and restores the exact previous match state.",9.5,{.55,.88,1,1});resumeInfo:SetPoint("TOPLEFT",28,-205);resumeInfo:SetWidth(520);resumeInfo:SetWordWrap(true)
        local resume=Btn(p,"Resume Match",145,34,true);resume:SetPoint("BOTTOMRIGHT",-28,28);resume:SetScript("OnClick",function()local ok,err=HS.AcceptResumeChallenge(id);if not ok then ShowStatus(err) end;sh:Hide()end)
        local fresh=Btn(p,"Start New",118,34,false);fresh:SetPoint("RIGHT",resume,"LEFT",-10,0);fresh:SetScript("OnClick",function()local ok,err=HS.AcceptChallenge(id);if not ok then ShowStatus(err) end;sh:Hide()end)
        local decline=Btn(p,"Decline",100,34,false);decline:SetPoint("RIGHT",fresh,"LEFT",-10,0);decline:SetScript("OnClick",function()HS.DeclineChallenge(id);sh:Hide()end)
    else
        local acceptLabel=ch.resumeMatchID and "Start New Instead" or "Accept"
        local accept=Btn(p,acceptLabel,ch.resumeMatchID and 164 or 140,34,true);accept:SetPoint("BOTTOMRIGHT",-36,28);accept:SetScript("OnClick",function()local ok,err=HS.AcceptChallenge(id);if not ok then ShowStatus(err) end;sh:Hide()end);local decline=Btn(p,"Decline",120,34,false);decline:SetPoint("RIGHT",accept,"LEFT",-12,0);decline:SetScript("OnClick",function()HS.DeclineChallenge(id);sh:Hide()end)
        if ch.resumeMatchID then local miss=Text(p,"They offered a paused match, but this client no longer has the matching local state. Resume is unavailable; you can explicitly start a new match instead.",8.8,{1,.55,.40,1});miss:SetPoint("BOTTOMLEFT",28,74);miss:SetWidth(500);miss:SetWordWrap(true) end
    end
end

-- ---------------- match table ----------------
local function MatchView(match,viewerSide)
    -- Local authoritative objects expose deck arrays; remote snapshots use deckCount.
    local me=viewerSide or match.viewerSide or 0;local mine=(me==1 or me==2) and match.players[me] or nil;local opp=(me==1 and match.players[2]) or (me==2 and match.players[1]) or match.players[1]
    return mine,opp,me
end
local function AccentFromHex(hex)
    hex=tostring(hex or "58D8FF");if #hex<6 then return P().gold end
    return {(tonumber(hex:sub(1,2),16) or 88)/255,(tonumber(hex:sub(3,4),16) or 216)/255,(tonumber(hex:sub(5,6),16) or 255)/255,1}
end
local function FindUnitByName(name)
    local units={"player","target","focus"};for i=1,4 do units[#units+1]="party"..i end;for i=1,40 do units[#units+1]="raid"..i end
    for _,u in ipairs(units) do if UnitExists and UnitExists(u) then local n=GetUnitName and GetUnitName(u,true) or UnitName(u);if n==name or (Ambiguate and Ambiguate(n or "","short")==Ambiguate(name or "","short")) then return u end end end
end
local function TargetSpec(kind,uid) return uid and (kind..":"..uid) or kind end

-- Match board pieces are persistent, reusable 2D tavern cards.  A fixed pool
-- of ten slots keeps network refreshes cheap while preserving card proportions.
local function CursorInsideFrame(frame)
    if not frame or not frame:IsShown() or not GetCursorPosition then return false end
    local l,r,b,t=frame:GetLeft(),frame:GetRight(),frame:GetBottom(),frame:GetTop();if not l or not r or not b or not t then return false end
    local x,y=GetCursorPosition();local scale=(UIParent and UIParent.GetEffectiveScale and UIParent:GetEffectiveScale()) or 1;x=x/scale;y=y/scale
    return x>=l and x<=r and y>=b and y<=t
end

local function FindCursorTarget(f,enemyOnly)
    for _,target in ipairs(f._targetFrames or {}) do
        if target and target:IsShown() and CursorInsideFrame(target) then
            local k=target._hsTargetKind
            if (not enemyOnly) or k=="enemyHero" or k=="enemyCreature" then return k,target._hsTargetUID,target end
        end
    end
end

local function ClearTargetHints(f)
    for _,target in ipairs(f._targetFrames or {}) do
        if target and target._hsTargetHint then target._hsTargetHint:SetAlpha(0) end
    end
end

local function IsPendingTargetValid(wanted,kind)
    if not wanted or wanted=="" or wanted=="none" then return true end
    if wanted=="enemy" then return kind=="enemyHero" or kind=="enemyCreature" end
    if wanted=="friendly" then return kind=="friendlyHero" or kind=="friendlyCreature" end
    if wanted=="enemyCreature" then return kind=="enemyCreature" end
    if wanted=="friendlyCreature" then return kind=="friendlyCreature" end
    if wanted=="anyCreature" then return kind=="enemyCreature" or kind=="friendlyCreature" end
    if wanted=="enemyHero" then return kind=="enemyHero" end
    if wanted=="friendlyHero" then return kind=="friendlyHero" end
    if wanted=="any" then return kind=="enemyHero" or kind=="friendlyHero" or kind=="enemyCreature" or kind=="friendlyCreature" end
    return false
end

local function BeginPendingTarget(f,action,a,card)
    ClearTargetHints(f)
    local wanted=card and card.target
    local validCount=0
    for _,target in ipairs(f._targetFrames or {}) do
        if IsPendingTargetValid(wanted,target._hsTargetKind) then
            validCount=validCount+1
            if target._hsTargetHint then target._hsTargetHint:SetAlpha(.68) end
        end
    end
    if validCount<=0 then
        f._pendingMode=nil;f._pendingAction=nil;f._pendingA=nil;f._pendingCard=nil;f._pendingPlayPosition=nil
        ShowStatus("There are no valid targets for "..tostring(card and card.name or "this ability")..".")
        return false
    end
    f._pendingMode="target";f._pendingAction=action;f._pendingA=a;f._pendingCard=card
    ShowStatus("Choose a target for "..tostring(card and card.name or "this card")..".")
    return true
end

local function EnsureTargetRope(f)
    if f._rope then return end
    f._rope=CreateFrame("Frame",nil,UIParent);f._rope:SetAllPoints(UIParent);f._rope:SetFrameStrata("TOOLTIP");f._rope:SetFrameLevel(8800);f._rope:Hide();f._rope:EnableMouse(false)
    f._ropeTex=f._rope:CreateTexture(nil,"OVERLAY");f._ropeTex:SetTexture(WHITE);f._ropeTex:SetColorTexture(1,.67,.18,.90);f._ropeTex:SetHeight(5)
    f._ropeHead=f._rope:CreateTexture(nil,"OVERLAY");f._ropeHead:SetTexture("Interface\\Buttons\\UI-ActionButton-Border");f._ropeHead:SetBlendMode("ADD");f._ropeHead:SetSize(34,34);f._ropeHead:SetVertexColor(1,.63,.12,1)
end
local function UpdateTargetRope(f)
    local src=f._ropeSource;if not src or not src:IsShown() then return end
    local sx,sy=src:GetCenter();if not sx or not sy then return end
    local x,y=GetCursorPosition();local scale=(UIParent:GetEffectiveScale() or 1);x=x/scale;y=y/scale
    local dx,dy=x-sx,y-sy;local len=math.sqrt(dx*dx+dy*dy);local angle=(math.atan2 and math.atan2(dy,dx)) or math.atan(dy,dx)
    f._ropeTex:ClearAllPoints();f._ropeTex:SetSize(math.max(1,len),5);f._ropeTex:SetPoint("CENTER",UIParent,"BOTTOMLEFT",sx+dx*.5,sy+dy*.5);if f._ropeTex.SetRotation then f._ropeTex:SetRotation(angle) end
    f._ropeHead:ClearAllPoints();f._ropeHead:SetPoint("CENTER",UIParent,"BOTTOMLEFT",x,y)
end
local CenteredBoardX, CursorBoardInsertIndex, SetPlacementGuide, UpdatePlacementGuide, ResetPlacementPreview

local function StartAttackDrag(f,slot,uid,canAttack)
    if not f or not slot or not canAttack then return false end
    f._attackDragUID=uid;f._attackDragCanAttack=true;f._ropeSource=slot
    EnsureTargetRope(f);f._rope:Show();f._rope:SetScript("OnUpdate",function()UpdateTargetRope(f)end)
    ShowStatus("Drag this creature onto an enemy to attack.")
    return true
end
local function FinishAttackDrag(f)
    if not f or not f._attackDragUID then return end
    local uid=f._attackDragUID;f._attackDragUID=nil;f._attackDragCanAttack=nil;f._ropeSource=nil
    if f._rope then f._rope:SetScript("OnUpdate",nil);f._rope:Hide() end
    local kind,targetUID=FindCursorTarget(f,true)
    if kind then local ok,err=HS.LocalAction("attack",uid,TargetSpec(kind,targetUID));if not ok and err then ShowStatus(err) end;return end
    ShowStatus("Attack cancelled.")
end

CenteredBoardX=function(count,index)
    count=math.max(1,math.min(HS.MAX_BOARD or 5,math.floor(tonumber(count) or 1)))
    index=math.max(1,math.min(count,math.floor(tonumber(index) or 1)))
    return (index-((count+1)*.5))*BOARD_SPACING
end

CursorBoardInsertIndex=function(f,finalCount)
    if not f or not f.ownBoardDrop or not CursorInsideFrame(f.ownBoardDrop) then return nil end
    finalCount=math.max(1,math.min(HS.MAX_BOARD or 5,math.floor(tonumber(finalCount) or 1)))
    local x=GetCursorPosition();local scale=UIParent:GetEffectiveScale() or 1;x=x/scale
    local cx=f.content and f.content:GetCenter() or nil;if not cx then return 1 end
    local best,bestDist=1,nil
    for i=1,finalCount do local px=cx+CenteredBoardX(finalCount,i);local d=math.abs(x-px);if not bestDist or d<bestDist then best,bestDist=i,d end end
    return best
end

SetPlacementGuide=function(f,index,count)
    if not f or not f.placeGuide then return end
    if not index or not count then f.placeGuide:Hide();return end
    f.placeGuide:ClearAllPoints();f.placeGuide:SetPoint("CENTER",f.content,"CENTER",CenteredBoardX(count,index),-80);f.placeGuide:Show()
end

local function EasePreviewSlot(slot,targetX,targetY)
    if not slot or not slot:IsShown() then return end
    local current=tonumber(slot._previewX);if current==nil then current=tonumber(slot._actualX) or targetX end
    current=current+(targetX-current)*.26
    if math.abs(targetX-current)<.35 then current=targetX end
    slot._previewX=current;slot:ClearAllPoints();slot:SetPoint("CENTER",slot._matchFrame.content,"CENTER",current,targetY or -80)
end

ResetPlacementPreview=function(f)
    if not f or not f._ownOrderedSlots then return end
    for _,slot in ipairs(f._ownOrderedSlots) do
        if slot and slot:IsShown() then local x=tonumber(slot._actualX) or 0;slot._previewX=x;slot:ClearAllPoints();slot:SetPoint("CENTER",f.content,"CENTER",x,-80) end
    end
end

local function PreviewPlacementLayout(f,index,finalCount)
    if not f or not index or not f._ownOrderedSlots then return end
    local slots=f._ownOrderedSlots
    if f._handDrag and f._handDrag.card and f._handDrag.card.type=="Creature" then
        for i,slot in ipairs(slots) do local desired=(i<index) and i or (i+1);EasePreviewSlot(slot,CenteredBoardX(finalCount,desired),-80) end
    end
end

UpdatePlacementGuide=function(f,count)
    local index=CursorBoardInsertIndex(f,count)
    SetPlacementGuide(f,index,count)
    if index then PreviewPlacementLayout(f,index,count) else ResetPlacementPreview(f) end
    return index
end

local function EnsureHandDragGhost(f)
    if f._dragGhost then return f._dragGhost end
    local g=CreateFrame("Frame",nil,UIParent,"BackdropTemplate");f._dragGhost=g;g:SetSize(132,174);g:SetFrameStrata("TOOLTIP");g:SetFrameLevel(8700);g:EnableMouse(false);g:Hide();g:SetBackdrop({bgFile=HS_LEATHER,tile=true,tileSize=64,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=9,insets={left=3,right=3,top=3,bottom=3}});g:SetBackdropColor(.16,.08,.035,.96)
    g.art=g:CreateTexture(nil,"ARTWORK");g.art:SetPoint("TOPLEFT",7,-7);g.art:SetPoint("TOPRIGHT",-7,-7);g.art:SetHeight(92);g.art:SetTexCoord(.08,.92,.08,.92)
    g.name=Text(g,"",10,P().pale,DISPLAY);g.name:SetPoint("TOP",0,-106);g.name:SetWidth(116);g.name:SetWordWrap(false);g.cost=Text(g,"",14,{.55,.82,1,1},DISPLAY);g.cost:SetPoint("TOPLEFT",7,-7)
    g.driver=CreateFrame("Frame",nil,UIParent);g.driver:Hide();g.driver:SetScript("OnUpdate",function()
        if not g:IsShown() then return end
        local x,y=GetCursorPosition();local scale=UIParent:GetEffectiveScale() or 1;g:ClearAllPoints();g:SetPoint("CENTER",UIParent,"BOTTOMLEFT",x/scale,y/scale)
        local mf=g._matchFrame;if mf and mf._handDrag and mf._handDrag.card and mf._handDrag.card.type=="Creature" then UpdatePlacementGuide(mf,math.min((HS.MAX_BOARD or 5),#(mf._handDrag.mine.board or {})+1)) elseif mf and mf.placeGuide then mf.placeGuide:Hide() end
    end)
    return g
end
local function IsDirectCardTargetValid(card,kind)
    if not card or not card.target or not kind then return false end
    if card.target=="enemy" then return kind=="enemyHero" or kind=="enemyCreature" end
    if card.target=="friendly" then return kind=="friendlyHero" or kind=="friendlyCreature" end
    if card.target=="enemyCreature" then return kind=="enemyCreature" end
    if card.target=="friendlyCreature" then return kind=="friendlyCreature" end
    return false
end
local function ShowDragTargets(f,card)
    ClearTargetHints(f)
    if not card or not card.target then return end
    for _,target in ipairs(f._targetFrames or {}) do
        if target and target._hsTargetHint and IsDirectCardTargetValid(card,target._hsTargetKind) then target._hsTargetHint:SetAlpha(.60) end
    end
end
local function StartHandDrag(f,card,index,mine,me,match)
    if match.finished or match.turnSide~=me then ShowStatus("It is not your turn.");return false end
    if (mine.mana or 0)<(card.cost or 0) then ShowStatus("Not enough Mana.");return false end
    local g=EnsureHandDragGhost(f);local rc=CardColor(card);g:SetBackdropBorderColor(rc[1],rc[2],rc[3],1);g.art:SetTexture(card.art or card.icon or "Interface\\Icons\\INV_Misc_QuestionMark");g.name:SetText(Short(card.name,22));g.cost:SetText(tostring(card.cost or 0));g._matchFrame=f;g:Show();g.driver:Show();f._handDrag={card=card,index=index,mine=mine}
    if f.playDrop then f.playDrop:SetAlpha(.72) end
    ShowDragTargets(f,card)
    return true
end
local function FinishHandDrag(f)
    local drag=f._handDrag;local card=drag and drag.card;local insertPos=nil
    if drag and card and card.type=="Creature" then insertPos=CursorBoardInsertIndex(f,math.min((HS.MAX_BOARD or 5),#(drag.mine.board or {})+1)) end
    -- Read a direct target before the drag state/ghost is cleared.  Action cards
    -- can now be dropped directly onto a hero/creature, while dropping them on
    -- the table still enters the traditional target-picking mode.
    local targetKind,targetUID=FindCursorTarget(f,false)
    f._handDrag=nil;local g=f._dragGhost;if g then g:Hide();g.driver:Hide();g._matchFrame=nil end;if f.playDrop then f.playDrop:SetAlpha(.08) end;if f.placeGuide then f.placeGuide:Hide() end;ResetPlacementPreview(f);ClearTargetHints(f)
    if not drag then return end
    if card.type=="Creature" and #(drag.mine.board or {})>=(HS.MAX_BOARD or 5) then ShowStatus("Your board is full.");return end
    local prefix=(card.type=="Creature" and insertPos) and ("P"..tostring(insertPos)..";") or ""
    if card.target and IsDirectCardTargetValid(card,targetKind) then
        local ok,err=HS.LocalAction("play",drag.index,prefix..TargetSpec(targetKind,targetUID));if not ok and err then ShowStatus(err) end;return
    end
    if not CursorInsideFrame(f.ownBoardDrop or f.playDrop) then
        ShowStatus(card.target and ("Drop "..card.name.." on a valid target or on your table to choose one.") or "Drag cards onto your side of the table to play them.");return
    end
    if card.target then f._pendingPlayPosition=(card.type=="Creature" and insertPos or nil);BeginPendingTarget(f,"play",drag.index,card)
    else local ok,err=HS.LocalAction("play",drag.index,prefix);if not ok and err then ShowStatus(err) end end
end

local function EnsureWeaponDragGhost(f)
    if f._weaponGhost then return f._weaponGhost end
    local g=CreateFrame("Frame",nil,UIParent,"BackdropTemplate");f._weaponGhost=g;g:SetSize(78,78);g:SetFrameStrata("TOOLTIP");g:SetFrameLevel(8750);g:EnableMouse(false);g:Hide();g:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=9,insets={left=3,right=3,top=3,bottom=3}});g:SetBackdropColor(.055,.025,.016,.96);g:SetBackdropBorderColor(1,.70,.28,.96)
    g.icon=g:CreateTexture(nil,"ARTWORK");g.icon:SetPoint("TOPLEFT",5,-5);g.icon:SetPoint("BOTTOMRIGHT",-5,5);g.icon:SetTexCoord(.08,.92,.08,.92)
    g.driver=CreateFrame("Frame",nil,UIParent);g.driver:Hide();g.driver:SetScript("OnUpdate",function()if not g:IsShown() then return end;local x,y=GetCursorPosition();local scale=UIParent:GetEffectiveScale() or 1;g:ClearAllPoints();g:SetPoint("CENTER",UIParent,"BOTTOMLEFT",x/scale,y/scale) end)
    return g
end
local function StartWeaponDrag(f,weapon,me,match)
    if not weapon or match.finished or match.turnSide~=me or weapon._attacked then return false end
    local card=HS.GetCard(weapon.card);local g=EnsureWeaponDragGhost(f);g.icon:SetTexture((card and (card.icon or card.art)) or "Interface\\Icons\\INV_Sword_04");g:Show();g.driver:Show();f._weaponDragging=true
    ClearTargetHints(f);for _,t in ipairs(f._targetFrames or {}) do if t._hsTargetHint and (t._hsTargetKind=="enemyHero" or t._hsTargetKind=="enemyCreature") then t._hsTargetHint:SetAlpha(.60) end end
    return true
end
local function FinishWeaponDrag(f)
    if not f._weaponDragging then return end;f._weaponDragging=nil
    local kind,uid=FindCursorTarget(f,true);local g=f._weaponGhost;if g then g:Hide();g.driver:Hide() end;ClearTargetHints(f)
    if not kind then ShowStatus("Weapon attack cancelled.");return end
    local ok,err=HS.LocalAction("heroattack",TargetSpec(kind,uid));if not ok and err then ShowStatus(err) end
end

local function FormatMatchDuration(seconds)
    seconds=math.max(0,math.floor(tonumber(seconds) or 0));return string.format("%d:%02d",math.floor(seconds/60),seconds%60)
end

local function SetBoardHover(slot,active,strong)
    if not slot then return end
    slot._hovered=active and true or false
    local face=slot.face or slot
    if active then
        slot:SetFrameLevel((slot._baseLevel or slot:GetFrameLevel())+35)
        face:SetScale(strong and 1.065 or 1.035)
        slot.shadow:SetAlpha(strong and .38 or .30);if slot.rarityAura then slot.rarityAura:SetAlpha(strong and 1.65 or 1.35) end
        if slot.hoverRing then slot.hoverRing:SetAlpha(1);if slot.hoverRing.glow then slot.hoverRing.glow:SetAlpha(.72) end end
        if not slot.tiltDriver then slot.tiltDriver=CreateFrame("Frame",nil,slot) end
        slot.tiltDriver:Show();slot.tiltDriver:SetScript("OnUpdate",function()
            if not slot._hovered or not slot:IsShown() then return end
            local l,r,b,t=slot:GetLeft(),slot:GetRight(),slot:GetBottom(),slot:GetTop();if not l or not r or not b or not t then return end
            local x,y=GetCursorPosition();local scale=UIParent:GetEffectiveScale() or 1;x=x/scale;y=y/scale
            local dx=math.max(-1,math.min(1,(x-(l+r)*.5)/math.max(1,(r-l)*.5)))
            local dy=math.max(-1,math.min(1,(y-(b+t)*.5)/math.max(1,(t-b)*.5)))
            face:ClearAllPoints();face:SetPoint("TOPLEFT",slot,"TOPLEFT",dx*2.4,5+dy*1.5);face:SetPoint("BOTTOMRIGHT",slot,"BOTTOMRIGHT",dx*2.4,5+dy*1.5)
            slot.shadow:ClearAllPoints();slot.shadow:SetPoint("TOPLEFT",9-dx*5,-9+dy*3);slot.shadow:SetPoint("BOTTOMRIGHT",-9-dx*5,7+dy*3)
        end)
    else
        slot:SetFrameLevel(slot._baseLevel or slot:GetFrameLevel());face:SetScale(1);slot.shadow:SetAlpha(.22);if slot.rarityAura then slot.rarityAura:SetAlpha(1) end
        if slot.hoverRing then slot.hoverRing:SetAlpha(0) end
        if slot.tiltDriver then slot.tiltDriver:SetScript("OnUpdate",nil);slot.tiltDriver:Hide() end
        face:ClearAllPoints();face:SetAllPoints(slot)
        slot.shadow:ClearAllPoints();slot.shadow:SetPoint("TOPLEFT",9,-9);slot.shadow:SetPoint("BOTTOMRIGHT",-9,7)
    end
end

local function PlayShake(target,amount)
    if not target or not target:IsShown() then return end;amount=amount or 7
    if target._hsShake and target._hsShake:IsPlaying() then target._hsShake:Stop() end
    local g=target._hsShake or target:CreateAnimationGroup();target._hsShake=g
    if not g._built then
        g.a=g:CreateAnimation("Translation");g.b=g:CreateAnimation("Translation");g.c=g:CreateAnimation("Translation");g.d=g:CreateAnimation("Translation");g._built=true
    end
    g.a:SetOrder(1);g.a:SetDuration(.045);g.a:SetOffset(amount,0)
    g.b:SetOrder(2);g.b:SetDuration(.045);g.b:SetOffset(-amount*2,0)
    g.c:SetOrder(3);g.c:SetDuration(.045);g.c:SetOffset(amount*1.5,0)
    g.d:SetOrder(4);g.d:SetDuration(.045);g.d:SetOffset(-amount*.5,0);g:Play()
end

local function PlayDamageBurst(target)
    if not target or not target:IsShown() then return end
    -- A contained slash-impact: amber core, two crossing streaks and outward
    -- sparks.  No full-card flash, so repeated hits remain comfortable.
    local impact=CreateFrame("Frame",nil,target);impact:SetSize(42,42);impact:SetPoint("CENTER",0,0);impact:SetFrameLevel(target:GetFrameLevel()+82);impact:EnableMouse(false)
    local core=impact:CreateTexture(nil,"OVERLAY");core:SetTexture(CELESTIAL.."starlight_glow.tga");core:SetAllPoints();core:SetBlendMode("ADD");core:SetVertexColor(1,.36,.055,.46)
    local slash1=impact:CreateTexture(nil,"OVERLAY");slash1:SetTexture(WHITE);slash1:SetSize(54,3);slash1:SetPoint("CENTER");slash1:SetColorTexture(1,.66,.16,.90);slash1:SetBlendMode("ADD");if slash1.SetRotation then slash1:SetRotation(.62) end
    local slash2=impact:CreateTexture(nil,"OVERLAY");slash2:SetTexture(WHITE);slash2:SetSize(38,2);slash2:SetPoint("CENTER",2,-2);slash2:SetColorTexture(1,.27,.06,.72);slash2:SetBlendMode("ADD");if slash2.SetRotation then slash2:SetRotation(-.72) end
    local ig=impact:CreateAnimationGroup();local grow=ig:CreateAnimation("Scale");grow:SetOrder(1);grow:SetDuration(.16);grow:SetScale(1.35,1.35);local fade=ig:CreateAnimation("Alpha");fade:SetOrder(1);fade:SetDuration(.20);fade:SetFromAlpha(.92);fade:SetToAlpha(0);ig:SetScript("OnFinished",function()impact:Hide();impact:SetParent(nil)end);ig:Play()
    for i=1,9 do
        local spark=CreateFrame("Frame",nil,target);spark:SetSize(2,7+((i*3)%7));spark:SetPoint("CENTER",0,0);spark:SetFrameLevel(target:GetFrameLevel()+84);spark:EnableMouse(false)
        local tex=spark:CreateTexture(nil,"OVERLAY");tex:SetTexture(WHITE);tex:SetAllPoints();tex:SetBlendMode("ADD");local bright=(i%3==0);tex:SetColorTexture(1,bright and .80 or .43,bright and .24 or .07,.84)
        local angle=((i-1)*6.2831853/9)+((i%2==0) and .12 or -.09);if tex.SetRotation then tex:SetRotation(angle+1.5707963) end
        local distance=22+(i%4)*7;local g=spark:CreateAnimationGroup();local move=g:CreateAnimation("Translation");move:SetOrder(1);move:SetDuration(.22+(i%3)*.025);move:SetOffset(math.cos(angle)*distance,math.sin(angle)*distance+5);local sf=g:CreateAnimation("Alpha");sf:SetOrder(1);sf:SetDuration(.25);sf:SetFromAlpha(.96);sf:SetToAlpha(0);g:SetScript("OnFinished",function()spark:Hide();spark:SetParent(nil)end);g:Play()
    end
end

local function PlaySoftBounce(frame,amount,delay)
    if not frame or not frame:IsShown() then return end
    local function run()
        if not frame or not frame:IsShown() then return end
        local g=frame:CreateAnimationGroup();local up=g:CreateAnimation("Translation");up:SetOrder(1);up:SetOffset(0,amount or 4);up:SetDuration(.055);up:SetSmoothing("OUT");local down=g:CreateAnimation("Translation");down:SetOrder(2);down:SetOffset(0,-(amount or 4));down:SetDuration(.085);down:SetSmoothing("IN");g:Play()
    end
    if delay and delay>0 and C_Timer and C_Timer.After then C_Timer.After(delay,run) else run() end
end
local function PlayArenaImpactKick(f)
    if not f then return end
    if f.surface then PlayShake(f.surface,1.5) end
end

local function PlayAttackLunge(source,target,impactCallback)
    if not source or not target or not source:IsShown() or not target:IsShown() then return end
    local sx,sy=source:GetCenter();local tx,ty=target:GetCenter();if not sx or not tx then return end
    local dx,dy=tx-sx,ty-sy;local length=math.max(1,math.sqrt(dx*dx+dy*dy));local nx,ny=dx/length,dy/length
    local targetRadius=math.max(22,math.min((target.GetWidth and target:GetWidth() or 90),(target.GetHeight and target:GetHeight() or 90))*.30)
    local travel=math.max(36,length-targetRadius);local ox,oy=nx*travel,ny*travel
    if source.face and SetBoardHover then SetBoardHover(source,false,false) end
    local actor=source.face or source;if actor._hsAttack and actor._hsAttack:IsPlaying() then actor._hsAttack:Stop() end
    local oldLevel=actor:GetFrameLevel() or 1;actor:SetFrameLevel(oldLevel+95)
    local g=actor:CreateAnimationGroup();actor._hsAttack=g
    local out=g:CreateAnimation("Translation");out:SetOrder(1);out:SetDuration(.19);out:SetOffset(ox,oy);out:SetSmoothing("IN")
    local hold=g:CreateAnimation("Translation");hold:SetOrder(2);hold:SetDuration(.045);hold:SetOffset(0,0)
    local back=g:CreateAnimation("Translation");back:SetOrder(3);back:SetDuration(.23);back:SetOffset(-ox,-oy);back:SetSmoothing("OUT")
    g:SetScript("OnFinished",function()if actor then actor:SetFrameLevel(oldLevel);actor._hsAttack=nil end;if source and source.face and source.IsMouseOver and source:IsMouseOver() then SetBoardHover(source,true,false) end end);g:Play()
    if impactCallback then if C_Timer and C_Timer.After then C_Timer.After(.18,impactCallback) else impactCallback() end end
end

local function PlayLandingPulse(f,frame)
    if not f or not frame or not frame:IsShown() then return end
    local pulse=CreateFrame("Frame",nil,f.fxLayer or f.content);pulse:SetSize(74,24);pulse:SetPoint("BOTTOM",frame,"BOTTOM",0,-3);pulse:SetFrameLevel((f.fxLayer and f.fxLayer:GetFrameLevel() or f:GetFrameLevel())+90);pulse:EnableMouse(false)
    local tex=pulse:CreateTexture(nil,"OVERLAY");tex:SetTexture(CELESTIAL.."starlight_round.tga");tex:SetBlendMode("ADD");tex:SetAllPoints();tex:SetVertexColor(1,.68,.24,.34)
    local g=pulse:CreateAnimationGroup();local sc=g:CreateAnimation("Scale");sc:SetScale(1.65,1.30);sc:SetDuration(.20);sc:SetSmoothing("OUT");local fa=g:CreateAnimation("Alpha");fa:SetFromAlpha(.70);fa:SetToAlpha(0);fa:SetDuration(.24);g:SetScript("OnFinished",function()pulse:Hide();pulse:SetParent(nil)end);g:Play()
end

local function FinalizeDropMatchPiece(frame,token,withImpact)
    if not frame then return end
    if token and frame._introDropping~=token then return end
    local home=frame._introHome
    -- Clear the logical drop state before stopping the animation.  Some WoW
    -- clients fire OnFinished/OnStop callbacks while Stop() is running.
    frame._introDropping=nil
    local anim=frame._introAnim;frame._introAnim=nil
    if anim and anim.IsPlaying and anim:IsPlaying() then pcall(anim.Stop,anim) end
    if frame.GetParent and frame:GetParent() and home then
        frame:ClearAllPoints()
        frame:SetPoint(home.point,home.relative,home.relativePoint,home.x,home.y)
        frame:SetAlpha(1)
        frame:Show()
        if frame.face then frame.face:SetAlpha(1);frame.face:Show() end
        if frame.artStage then frame.artStage:SetAlpha(1);frame.artStage:Show() end
        if frame.art then frame.art:SetAlpha(1);frame.art:Show() end
        -- Deployment keeps the new card's own landing pulse, but never shakes
        -- the arena surface. Shaking the shared surface made every existing
        -- board card appear to bounce whenever a new card was deployed.
        if withImpact and home.owner then PlayLandingPulse(home.owner,frame) end
    end
    frame._introHome=nil
end

local function DropMatchPiece(f,frame,dropDistance,delay)
    if not f or not frame or not frame:IsShown() then return end
    if frame._introDropping then FinalizeDropMatchPiece(frame,frame._introDropping,false) end
    local point,relative,relativePoint,x,y=frame:GetPoint(1);if not point then return end
    relative=relative or frame:GetParent();relativePoint=relativePoint or point;x=tonumber(x) or 0;y=tonumber(y) or 0
    local distance=dropDistance or 180;local duration=.30+math.min(.16,distance/1000)
    frame._introToken=(tonumber(frame._introToken) or 0)+1
    local token=frame._introToken
    frame._introDropping=token
    frame._introHome={point=point,relative=relative,relativePoint=relativePoint,x=x,y=y,owner=f}
    frame:ClearAllPoints();frame:SetPoint(point,relative,relativePoint,x,y+distance);frame:SetAlpha(0)
    local function begin()
        if not frame or frame._introDropping~=token or not frame:GetParent() then return end
        frame:SetAlpha(.08);frame:Show()
        local g=frame:CreateAnimationGroup();frame._introAnim=g
        local tr=g:CreateAnimation("Translation");tr:SetOrder(1);tr:SetOffset(0,-distance);tr:SetDuration(duration);tr:SetSmoothing("IN")
        local al=g:CreateAnimation("Alpha");al:SetOrder(1);al:SetFromAlpha(.08);al:SetToAlpha(1);al:SetDuration(math.min(.22,duration))
        g:SetScript("OnFinished",function()FinalizeDropMatchPiece(frame,token,true) end)
        g:Play()
    end
    if C_Timer and C_Timer.After then
        C_Timer.After(delay or 0,begin)
        -- Hard landing recovery.  The old intro could leave a single hand or
        -- board card suspended/invisible if an animation callback was skipped
        -- during the first state sync.  This timer always restores its home.
        C_Timer.After((delay or 0)+duration+.16,function()FinalizeDropMatchPiece(frame,token,false) end)
    else begin() end
end

local function PlayMatchEntrance(f,pieces)
    if not f then return end
    if f.surface then f.surface:SetAlpha(.82);local sg=f.surface:CreateAnimationGroup();local sa=sg:CreateAnimation("Alpha");sa:SetFromAlpha(.82);sa:SetToAlpha(1);sa:SetDuration(.28);sg:Play() end
end

local function SnapshotMatch(match)
    local snap={turnSide=match and match.turnSide or 0,actionSeq=match and match.lastAction and match.lastAction.seq or 0,players={}}
    for side=1,2 do
        local p=match and match.players and match.players[side] or nil;local out={hp=p and p.hp or 0,armor=p and p.armor or 0,board={}}
        for _,u in ipairs(p and p.board or {}) do out.board[tonumber(u.uid) or 0]={hp=u.hp,canAttack=u.canAttack} end
        snap.players[side]=out
    end
    return snap
end

local function EnsureTurnBanner(f)
    if f.turnBanner then return f.turnBanner end
    local b=CreateFrame("Frame",nil,f,"BackdropTemplate");b:SetSize(420,82);b:SetPoint("CENTER",0,10);b:SetFrameLevel(f:GetFrameLevel()+140);b:EnableMouse(false);Backdrop(b,P().gold,.995);b:SetBackdropColor(.095,.045,.018,.98);b:Hide()
    b.text=Text(b,"",24,P().gold,DISPLAY);b.text:SetPoint("CENTER",0,6);b.sub=Text(b,"",9.2,P().muted);b.sub:SetPoint("TOP",b.text,"BOTTOM",0,-1)
    b.anim=b:CreateAnimationGroup()
    local fi=b.anim:CreateAnimation("Alpha");fi:SetOrder(1);fi:SetFromAlpha(0);fi:SetToAlpha(1);fi:SetDuration(.22);fi:SetSmoothing("OUT")
    local hold=b.anim:CreateAnimation("Alpha");hold:SetOrder(2);hold:SetFromAlpha(1);hold:SetToAlpha(1);hold:SetDuration(.92)
    local fo=b.anim:CreateAnimation("Alpha");fo:SetOrder(3);fo:SetFromAlpha(1);fo:SetToAlpha(0);fo:SetDuration(.30);fo:SetSmoothing("IN")
    b.anim:SetScript("OnFinished",function()b:Hide();b:SetAlpha(1)end)
    f.turnBanner=b;return b
end

local function ShowTurnBanner(f,isMine,key)
    local b=EnsureTurnBanner(f);key=key or tostring(isMine)
    if b._lastKey==key then return end
    b._lastKey=key
    if b.anim:IsPlaying() then b.anim:Stop() end
    b.text:SetText(isMine and "YOUR TURN" or "OPPONENT'S TURN");b.sub:SetText(isMine and "The table is yours." or "Waiting for the other player.")
    b:SetAlpha(1);b:Show();b:Raise();b.anim:Play()
end

local function CreateMatchBoardSlot(parent)
    local slot=CreateFrame("Button",nil,parent);slot:SetSize(136,202);slot:Hide();slot:SetClipsChildren(false);slot:RegisterForDrag("LeftButton");slot:SetFrameLevel((parent:GetFrameLevel() or 1)+12);slot._baseLevel=slot:GetFrameLevel()
    slot.rarityAura=slot:CreateTexture(nil,"BACKGROUND",nil,-2);slot.rarityAura:SetTexture(CELESTIAL.."starlight_glow.tga");slot.rarityAura:SetBlendMode("ADD");slot.rarityAura:SetPoint("TOPLEFT",-17,17);slot.rarityAura:SetPoint("BOTTOMRIGHT",17,-17);slot.rarityAura:SetVertexColor(.8,.8,.8,.08)
    slot.shadow=slot:CreateTexture(nil,"BACKGROUND");slot.shadow:SetTexture(WHITE);slot.shadow:SetPoint("TOPLEFT",9,-9);slot.shadow:SetPoint("BOTTOMRIGHT",-9,7);slot.shadow:SetColorTexture(0,0,0,.22)
    slot.face=CreateFrame("Frame",nil,slot,"BackdropTemplate");slot.face:SetAllPoints(slot);slot.face:EnableMouse(false)
    slot.face:SetBackdrop({bgFile=HS_LEATHER,tile=true,tileSize=96,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=9,insets={left=2,right=2,top=2,bottom=2}});slot.face:SetBackdropColor(.215,.112,.055,.995);slot.face:SetBackdropBorderColor(.58,.36,.17,.96)
    slot.faceGlow=slot.face:CreateTexture(nil,"BACKGROUND");slot.faceGlow:SetTexture(CELESTIAL.."starlight_round.tga");slot.faceGlow:SetBlendMode("ADD");slot.faceGlow:SetSize(150,150);slot.faceGlow:SetPoint("TOP",0,10);slot.faceGlow:SetVertexColor(.7,.7,.7,.05)
    slot.watermark=slot.face:CreateTexture(nil,"BACKGROUND");slot.watermark:SetTexture(ORNAMENT_SEAL);slot.watermark:SetSize(92,92);slot.watermark:SetPoint("CENTER",0,-47);slot.watermark:SetVertexColor(.8,.6,.3,.06)
    AddCardCornerChrome(slot.face,{rarity="Common"},.32)
    slot.artStage=CreateFrame("Frame",nil,slot.face,"BackdropTemplate");slot.artStage:SetPoint("TOPLEFT",12,-12);slot.artStage:SetPoint("TOPRIGHT",-12,-12);slot.artStage:SetHeight(92);slot.artStage:SetBackdrop({bgFile=HS_WOOD,tile=true,tileSize=96,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=6,insets={left=2,right=2,top=2,bottom=2}});slot.artStage:SetBackdropColor(.12,.055,.023,.99);slot.artStage:SetBackdropBorderColor(.57,.35,.17,.92);slot.artStage:SetClipsChildren(true)
    slot.art=slot.artStage:CreateTexture(nil,"ARTWORK");slot.art:SetPoint("TOPLEFT",4,-4);slot.art:SetPoint("BOTTOMRIGHT",-4,4);slot.art:SetTexCoord(.13,.87,.11,.89)
    slot.ribbon=CreateFrame("Frame",nil,slot.face,"BackdropTemplate");slot.ribbon:SetPoint("TOPLEFT",10,-109);slot.ribbon:SetPoint("TOPRIGHT",-10,-109);slot.ribbon:SetHeight(22);slot.ribbon:SetBackdrop({bgFile=HS_PARCHMENT,tile=true,tileSize=64,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=6,insets={left=2,right=2,top=2,bottom=2}});slot.ribbon:SetBackdropColor(.86,.70,.46,.99);slot.ribbon:SetBackdropBorderColor(.58,.36,.17,.92)
    slot.name=CardText(slot.ribbon,"",7.8,{.16,.07,.025,1},DISPLAY);slot.name:SetPoint("CENTER",0,1);slot.name:SetWidth(110);slot.name:SetJustifyH("CENTER");slot.name:SetWordWrap(false)
    slot.metaHost=CreateFrame("Frame",nil,slot.face);slot.metaHost:SetPoint("TOPLEFT",14,-136);slot.metaHost:SetPoint("TOPRIGHT",-14,-136);slot.metaHost:SetHeight(14)
    slot.typeText=CardText(slot.metaHost,"CREATURE",6.1,{.78,.64,.46,1},DISPLAY);slot.typeText:SetPoint("LEFT",2,0);slot.typeText:SetWidth(98);slot.typeText:SetJustifyH("LEFT")
    slot.rules=CardText(slot.face,"",6.25,{.86,.75,.58,1});slot.rules:SetPoint("TOPLEFT",14,-151);slot.rules:SetPoint("TOPRIGHT",-14,-151);slot.rules:SetHeight(18);slot.rules:SetJustifyH("CENTER");slot.rules:SetJustifyV("TOP");slot.rules:SetWordWrap(true)
    slot.atk=MakeStatBadge(slot.face,0,"attack",136,202);slot.atk:SetPoint("BOTTOMLEFT",3,1);slot.hp=MakeStatBadge(slot.face,0,"health",136,202);slot.hp:SetPoint("BOTTOMRIGHT",-3,1)
    slot.status=CreateFrame("Frame",nil,slot.face,"BackdropTemplate");slot.status:SetSize(50,18);slot.status:SetPoint("TOPRIGHT",-7,-7);slot.status:SetBackdrop({bgFile=HS_LEATHER,tile=true,tileSize=64,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=5,insets={left=2,right=2,top=2,bottom=2}});slot.status:SetBackdropColor(.18,.09,.035,.96);slot.status:SetBackdropBorderColor(.62,.38,.16,.82)
    slot.keyword=CardText(slot.status,"",5.7,{1,.80,.36,1},DISPLAY);slot.keyword:SetPoint("CENTER");slot.status:Hide()
    slot.rarityTop=slot.face:CreateTexture(nil,"BORDER");slot.rarityTop:SetTexture(WHITE);slot.rarityTop:SetPoint("TOPLEFT",8,-6);slot.rarityTop:SetPoint("TOPRIGHT",-8,-6);slot.rarityTop:SetHeight(3);slot.rarityTop:SetColorTexture(.75,.75,.75,.72)
    slot.raritySide=slot.face:CreateTexture(nil,"BORDER");slot.raritySide:SetTexture(WHITE);slot.raritySide:SetPoint("TOPLEFT",6,-12);slot.raritySide:SetPoint("BOTTOMLEFT",6,14);slot.raritySide:SetWidth(2);slot.raritySide:SetColorTexture(.75,.75,.75,.28)
    slot.hoverRing=AddOuterHoverRing(slot.face,{rarity="Common"},6)
    slot.readyRing=CreateFrame("Frame",nil,slot.face,"BackdropTemplate");slot.readyRing:SetPoint("TOPLEFT",-4,4);slot.readyRing:SetPoint("BOTTOMRIGHT",4,-4);slot.readyRing:EnableMouse(false);slot.readyRing:SetBackdrop({edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=11});slot.readyRing:SetBackdropBorderColor(.35,1,.48,.92);slot.readyRing:SetAlpha(0)
    slot.targetRing=CreateFrame("Frame",nil,slot.face,"BackdropTemplate");slot.targetRing:SetPoint("TOPLEFT",-6,6);slot.targetRing:SetPoint("BOTTOMRIGHT",6,-6);slot.targetRing:EnableMouse(false);slot.targetRing:SetBackdrop({edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=12});slot.targetRing:SetBackdropBorderColor(1,.67,.18,1);slot.targetRing:SetAlpha(0);slot._hsTargetHint=slot.targetRing
    slot:SetScript("OnEnter",function(self)if self._card and ShowCardInspect then ShowCardInspect(self._card,self) end;SetBoardHover(self,true,false) end)
    slot:SetScript("OnLeave",function(self)if HideCardInspect then HideCardInspect(self) end;if not self._dragging then SetBoardHover(self,false,false) end end)
    slot:SetScript("OnDragStart",function(self)local f=self._matchFrame;if not f or not self._isMine or not self._unit or not self._unit.canAttack or self._spectator or self._turnSide~=self._viewerSide then return end;self._dragging=true;SetBoardHover(self,true,true);StartAttackDrag(f,self,self._unit.uid,true) end)
    slot:SetScript("OnDragStop",function(self)self._dragging=nil;SetBoardHover(self,self:IsMouseOver(),false);local f=self._matchFrame;if f then FinishAttackDrag(f) end end)
    return slot
end

local function HideMatchBoardSlot(slot)
    if not slot then return end;slot._card=nil;slot._unit=nil;slot._onClick=nil;slot.readyRing:SetAlpha(0);slot.targetRing:SetAlpha(0);slot._dragging=nil;SetBoardHover(slot,false,false);slot:Hide()
end

local function UpdateMatchBoardSlot(slot,unit,card,isMine,onClick)
    if not slot or not unit or not card then HideMatchBoardSlot(slot);return end
    slot._card=card;slot._unit=unit;slot._isMine=isMine;slot._onClick=onClick
    -- Board slots are recycled.  Normalize every visual layer so a stale intro,
    -- death or hover animation can never leave the left-most slot transparent.
    slot:SetAlpha(1);slot:Show()
    if slot.face then slot.face:SetAlpha(1);slot.face:Show() end
    if slot.artStage then slot.artStage:SetAlpha(1);slot.artStage:Show() end
    if slot.art then slot.art:SetAlpha(1);slot.art:Show() end
    slot._hsTargetKind=isMine and "friendlyCreature" or "enemyCreature";slot._hsTargetUID=unit.uid
    local rc=CardColor(card);slot.face:SetBackdropBorderColor(.42+rc[1]*.32,.25+rc[2]*.32,.12+rc[3]*.32,.98);slot.readyRing:SetAlpha(isMine and unit.canAttack and .55 or 0)
    if slot.hoverRing then slot.hoverRing:SetBackdropBorderColor(rc[1],rc[2],rc[3],1);if slot.hoverRing.glow then slot.hoverRing.glow:SetVertexColor(rc[1],rc[2],rc[3],.68) end end
    slot.name:SetText(Short(card.name,19));slot.rules:SetText(card.text or "");slot.atk.value:SetText(tostring(unit.attack or 0));slot.hp.value:SetText(tostring(unit.hp or 0));slot.art:SetTexture(card.art or card.icon or "Interface\\Icons\\INV_Misc_QuestionMark")
    if slot.rarityTop then slot.rarityTop:SetColorTexture(rc[1],rc[2],rc[3],1) end;if slot.raritySide then slot.raritySide:SetColorTexture(rc[1],rc[2],rc[3],.72) end;if slot.rarityAura then local a=card.rarity=="Legendary" and .30 or (card.rarity=="Epic" and .20 or (card.rarity=="Rare" and .12 or .035));slot.rarityAura:SetVertexColor(rc[1],rc[2],rc[3],a);slot._rarityAuraAlpha=a end;if slot.faceGlow then slot.faceGlow:SetVertexColor(rc[1],rc[2],rc[3],card.rarity=="Legendary" and .20 or (card.rarity=="Epic" and .13 or .07)) end;if slot.watermark then slot.watermark:SetVertexColor(rc[1],rc[2],rc[3],.08) end;slot.typeText:SetText(string.upper(card.type or "CREATURE"));slot.typeText:SetTextColor(rc[1],rc[2],rc[3],1)
    local maxhp=tonumber(unit.maxhp or card.health or 0) or 0;slot.hp.value:SetTextColor(1,1,1,1);if slot.hp.icon then if (tonumber(unit.hp) or 0)<maxhp then slot.hp.icon:SetVertexColor(1,.34,.25,1) else slot.hp.icon:SetVertexColor(1,.24,.20,.96) end end
    if unit.taunt then slot.keyword:SetText("TAUNT");slot.keyword:SetTextColor(1,.78,.30,1);slot.status:SetBackdropBorderColor(.83,.54,.18,.94);slot.status:Show()
    elseif unit.canAttack then slot.keyword:SetText("READY");slot.keyword:SetTextColor(.50,1,.60,1);slot.status:SetBackdropBorderColor(.30,.82,.42,.94);slot.status:Show()
    else slot.status:Hide() end
    slot:SetScript("OnClick",function(self)if self._onClick then self._onClick() end end)
end

local function PlayCardDeath(f,slot)
    if not f or not f.boardLayer or not slot or not slot:IsShown() or not slot._card then return end
    local card=slot._card;local unit=slot._unit or {}
    local ghost=CreateFrame("Frame",nil,f.fxLayer or f.boardLayer,"BackdropTemplate")
    ghost:SetSize(slot:GetWidth() or 136,slot:GetHeight() or 202);ghost:SetPoint("CENTER",slot,"CENTER",0,0);ghost:SetFrameLevel((slot:GetFrameLevel() or 1)+70);ghost:EnableMouse(false)
    ghost:SetBackdrop({bgFile=HS_LEATHER,tile=true,tileSize=96,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=9,insets={left=2,right=2,top=2,bottom=2}});ghost:SetBackdropColor(.255,.145,.073,.995);ghost:SetBackdropBorderColor(.68,.32,.16,.98)
    local artBox=CreateFrame("Frame",nil,ghost,"BackdropTemplate");artBox:SetPoint("TOPLEFT",12,-12);artBox:SetPoint("TOPRIGHT",-12,-12);artBox:SetHeight(92);artBox:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=6,insets={left=2,right=2,top=2,bottom=2}});artBox:SetBackdropColor(.12,.055,.023,.99);artBox:SetBackdropBorderColor(.57,.35,.17,.92);artBox:SetClipsChildren(true)
    local art=artBox:CreateTexture(nil,"ARTWORK");art:SetPoint("TOPLEFT",4,-4);art:SetPoint("BOTTOMRIGHT",-4,4);art:SetTexture(card.art or card.icon or "Interface\\Icons\\INV_Misc_QuestionMark");art:SetTexCoord(.10,.90,.08,.92)
    local ribbon=CreateFrame("Frame",nil,ghost,"BackdropTemplate");ribbon:SetPoint("TOPLEFT",10,-109);ribbon:SetPoint("TOPRIGHT",-10,-109);ribbon:SetHeight(22);ribbon:SetBackdrop({bgFile=HS_PARCHMENT,tile=true,tileSize=64,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=6,insets={left=2,right=2,top=2,bottom=2}});ribbon:SetBackdropColor(.86,.70,.46,.99);ribbon:SetBackdropBorderColor(.58,.36,.17,.92)
    local name=CardText(ribbon,Short(card.name,19),8.2,{.16,.07,.025,1},FONT);name:SetPoint("CENTER",0,1);name:SetWidth(110);name:SetJustifyH("CENTER")
    local atk=MakeStatBadge(ghost,unit.attack or card.attack or 0,"attack",136,202);atk:SetPoint("BOTTOMLEFT",3,1)
    local hp=MakeStatBadge(ghost,math.max(0,tonumber(unit.hp) or 0),"health",136,202);hp:SetPoint("BOTTOMRIGHT",-3,1)
    local red=CreateFrame("Frame",nil,ghost);red:SetAllPoints();red:SetFrameLevel(ghost:GetFrameLevel()+10);red:EnableMouse(false)
    local redTex=red:CreateTexture(nil,"OVERLAY");redTex:SetAllPoints();redTex:SetTexture(WHITE);redTex:SetColorTexture(1,.025,.015,.92);red:SetAlpha(1)
    local redAnim=red:CreateAnimationGroup();local redIn=redAnim:CreateAnimation("Alpha");redIn:SetOrder(1);redIn:SetDuration(.58);redIn:SetFromAlpha(0);redIn:SetToAlpha(.92);redIn:SetSmoothing("IN")
    local g=ghost:CreateAnimationGroup();local rise=g:CreateAnimation("Translation");rise:SetOrder(1);rise:SetDuration(.68);rise:SetOffset(0,34);rise:SetSmoothing("OUT");local fade=g:CreateAnimation("Alpha");fade:SetOrder(1);fade:SetDuration(.68);fade:SetFromAlpha(1);fade:SetToAlpha(0);fade:SetSmoothing("IN")
    g:SetScript("OnFinished",function()ghost:Hide();ghost:SetParent(nil)end);redAnim:Play();g:Play()
    return ghost
end

local function CreatePlayedSpotlight(parent,matchRoot)
    -- Keep the played-card toast away from both deck wells.  It now lives on
    -- UIParent and is anchored just outside the left edge of the match table.
    -- Clamping is only a safety net for narrow resolutions; on normal layouts
    -- it remains physically outside the table instead of covering the arena.
    local root=matchRoot or parent
    local sp=CreateFrame("Frame",nil,UIParent,"BackdropTemplate");sp:SetSize(330,88);sp:SetPoint("TOPRIGHT",root,"TOPLEFT",-10,-74);sp:SetFrameStrata("FULLSCREEN_DIALOG");sp:SetFrameLevel((root.GetFrameLevel and root:GetFrameLevel() or 5400)+35);sp:SetClampedToScreen(true);sp:EnableMouse(false);sp:SetBackdrop({bgFile=HS_LEATHER,tile=true,tileSize=128,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=10,insets={left=2,right=2,top=2,bottom=2}});sp:SetBackdropColor(.13,.068,.032,.985);sp:SetBackdropBorderColor(P().gold[1],P().gold[2],P().gold[3],.92);sp:Hide()
    sp.artBox=CreateFrame("Frame",nil,sp,"BackdropTemplate");sp.artBox:SetSize(72,76);sp.artBox:SetPoint("LEFT",9,0);sp.artBox:SetBackdrop({bgFile=HS_WOOD,tile=true,tileSize=64,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=7,insets={left=2,right=2,top=2,bottom=2}});sp.artBox:SetBackdropColor(.08,.035,.015,.99);sp.artBox:SetBackdropBorderColor(.48,.29,.15,.85)
    sp.art=sp.artBox:CreateTexture(nil,"ARTWORK");sp.art:SetPoint("TOPLEFT",4,-4);sp.art:SetPoint("BOTTOMRIGHT",-4,4);sp.art:SetTexCoord(.08,.92,.08,.92)
    sp.kicker=Text(sp,"CARD PLAYED",7,P().muted,DISPLAY);sp.kicker:SetPoint("TOPLEFT",94,-8);sp.name=Text(sp,"",12.5,P().gold,DISPLAY);sp.name:SetPoint("TOPLEFT",94,-24);sp.name:SetWidth(216);sp.name:SetWordWrap(false);sp.meta=Text(sp,"",6.7,P().muted);sp.meta:SetPoint("TOPLEFT",94,-45);sp.rules=CardText(sp,"",7.4,P().pale);sp.rules:SetPoint("TOPLEFT",94,-61);sp.rules:SetWidth(216);sp.rules:SetHeight(23);sp.rules:SetWordWrap(true)
    sp.fadeIn=sp:CreateAnimationGroup();local fi=sp.fadeIn:CreateAnimation("Alpha");fi:SetFromAlpha(0);fi:SetToAlpha(1);fi:SetDuration(.16);sp.fadeOut=sp:CreateAnimationGroup();local fo=sp.fadeOut:CreateAnimation("Alpha");fo:SetFromAlpha(1);fo:SetToAlpha(0);fo:SetDuration(.22);sp.fadeOut:SetScript("OnFinished",function()sp:Hide();sp:SetAlpha(1) end);return sp
end

local function PlayLegendaryCardEffect(f,card)
    if not f or not card or card.rarity~="Legendary" or not f.fxLayer then return end
    local stage=CreateFrame("Frame",nil,f.fxLayer);stage:SetAllPoints(f.content);stage:SetFrameLevel(f.fxLayer:GetFrameLevel()+90);stage:EnableMouse(false)
    local halo=stage:CreateTexture(nil,"OVERLAY");halo:SetTexture(CELESTIAL.."starlight_round.tga");halo:SetBlendMode("ADD");halo:SetSize(320,320);halo:SetPoint("CENTER",0,8);halo:SetVertexColor(1,.57,.08,.58)
    local core=stage:CreateTexture(nil,"OVERLAY");core:SetTexture(CELESTIAL.."starlight_glow.tga");core:SetBlendMode("ADD");core:SetSize(190,190);core:SetPoint("CENTER",0,8);core:SetVertexColor(1,.86,.34,.74)
    local ag=stage:CreateAnimationGroup();local hs=ag:CreateAnimation("Scale");hs:SetOrder(1);hs:SetDuration(.42);hs:SetScale(2.0,2.0);hs:SetSmoothing("OUT");local hf=ag:CreateAnimation("Alpha");hf:SetOrder(1);hf:SetDuration(.52);hf:SetFromAlpha(.92);hf:SetToAlpha(0);ag:SetScript("OnFinished",function()stage:Hide();stage:SetParent(nil)end);ag:Play()
    for i=1,16 do
        local shard=CreateFrame("Frame",nil,f.fxLayer);shard:SetSize(3,11+(i%4)*4);shard:SetPoint("CENTER",f.content,"CENTER",0,8);shard:SetFrameLevel(f.fxLayer:GetFrameLevel()+92);shard:EnableMouse(false)
        local tex=shard:CreateTexture(nil,"OVERLAY");tex:SetAllPoints();tex:SetTexture(WHITE);tex:SetBlendMode("ADD");tex:SetColorTexture(1,(i%3==0) and .92 or .62,.16,.92)
        local angle=((i-1)*6.2831853/16)+((i%2)*.08);if tex.SetRotation then tex:SetRotation(angle+1.5707963) end
        local dist=92+(i%5)*16;local g=shard:CreateAnimationGroup();local move=g:CreateAnimation("Translation");move:SetOrder(1);move:SetDuration(.38+(i%4)*.035);move:SetOffset(math.cos(angle)*dist,math.sin(angle)*dist);move:SetSmoothing("OUT");local fade=g:CreateAnimation("Alpha");fade:SetOrder(1);fade:SetDuration(.48);fade:SetFromAlpha(1);fade:SetToAlpha(0);g:SetScript("OnFinished",function()shard:Hide();shard:SetParent(nil)end);g:Play()
    end
    if f.surface then PlayShake(f.surface,2.4) end
end

local function ShowPlayedSpotlight(f,match)
    local lp=match and match.lastPlayed;if not lp or not lp.card or not lp.seq then return end;local token=tostring(match.id or "")..":"..tostring(lp.seq);if f._lastPlayedSpotToken==token then return end;f._lastPlayedSpotToken=token
    local card=HS.GetCard(lp.card);local sp=f.playedSpotlight;if not card or not sp then return end;if sp.fadeOut:IsPlaying() then sp.fadeOut:Stop() end;if sp.fadeIn:IsPlaying() then sp.fadeIn:Stop() end
    sp._token=token;sp:SetAlpha(1);sp:Show();sp:Raise();local who=match.players and match.players[tonumber(lp.side) or 0];sp.kicker:SetText((who and who.display and (Short(who.display,24).." PLAYED") or "CARD PLAYED"));sp.name:SetText(card.name or "Card");sp.meta:SetText(string.format("%s  ·  %s  ·  %d MANA",string.upper(card.rarity or ""),string.upper(card.type or ""),tonumber(card.cost) or 0));sp.rules:SetText(card.text or "");sp.art:SetTexture(card.art or card.icon or "Interface\\Icons\\INV_Misc_QuestionMark");sp.fadeIn:Play();if card.rarity=="Legendary" then PlayLegendaryCardEffect(f,card) end
    if C_Timer and C_Timer.After then C_Timer.After(2.4,function()if sp and sp:IsShown() and sp._token==token then sp:SetAlpha(1);sp.fadeOut:Play() end end) end
end

local function ApplyArenaHalves(f,topPlayer,bottomPlayer)
    if not f then return end
    local top=ArenaDef(topPlayer and topPlayer.arena or "tavern")
    local bottom=ArenaDef(bottomPlayer and bottomPlayer.arena or "tavern")

    local function paintHalf(prefix,def)
        local rail=f[prefix.."Rail"]
        local lip=f[prefix.."RailLip"]
        local wash=f[prefix.."ArenaWash"]
        local pattern=f[prefix.."ArenaPattern"]
        local crest=f[prefix.."ArenaCrest"]
        local wing=f[prefix.."ArenaWing"]
        local glow=f[prefix.."ArenaGlow"]
        local trim=f[prefix.."ArenaTrim"]
        local name=f[prefix.."ArenaName"]
        local well=f[prefix.."DeckWell"]
        local lane=f[prefix.."LanePanel"]
        if rail then rail:SetColorTexture(def.rail[1],def.rail[2],def.rail[3],1) end
        if lip then lip:SetColorTexture(def.accent[1],def.accent[2],def.accent[3],.70) end
        if wash then wash:SetColorTexture(def.field[1],def.field[2],def.field[3],.18) end
        if pattern then pattern:SetTexture(ArenaPattern(def));pattern:SetVertexColor(def.accent[1],def.accent[2],def.accent[3],.36);pattern:SetAlpha(.075) end
        if crest then crest:SetTexture(OrnamentForArena(def,true));crest:SetVertexColor(def.accent[1],def.accent[2],def.accent[3],.22) end
        if wing then wing:SetColorTexture(def.accent[1],def.accent[2],def.accent[3],.12) end
        if glow then glow:SetVertexColor(def.accent[1],def.accent[2],def.accent[3],.075) end
        if trim then trim:SetColorTexture(def.accent[1],def.accent[2],def.accent[3],.34) end
        if name then name:SetText(string.upper(def.name or "ARENA"));name:SetTextColor(def.accent[1],def.accent[2],def.accent[3],.72) end
        if well and well.SetBackdropBorderColor then well:SetBackdropBorderColor(def.accent[1],def.accent[2],def.accent[3],.58) end
        if lane and lane.SetBackdropBorderColor then lane:SetBackdropBorderColor(def.accent[1],def.accent[2],def.accent[3],.36) end
    end
    paintHalf("top",top)
    paintHalf("bottom",bottom)
end

local function PlayerDeckCount(p)
    if not p then return 0 end
    if p.deckCount~=nil then return math.max(0,tonumber(p.deckCount) or 0) end
    return #(p.deck or {})
end

local function CreateDeckPile(parent,p,top,well)
    -- Treat the recessed well as the single layout authority.  The pile used
    -- to be independently offset inside a same-sized holder, which made the
    -- stack look crooked even when the holder itself was technically centered.
    local holder=Dynamic(CreateFrame("Frame",nil,parent));holder:SetSize(94,120);holder:SetFrameLevel(parent:GetFrameLevel()+10)
    if well then
        holder:SetPoint("CENTER",well,"CENTER",0,0)
    elseif top then
        holder:SetPoint("TOPRIGHT",parent,"TOPRIGHT",-66,-118)
    else
        holder:SetPoint("BOTTOMRIGHT",parent,"BOTTOMRIGHT",-66,114)
    end

    -- A dedicated stack anchor means the actual card geometry (not the holder,
    -- count label or caption) is what sits on the exact center of the well.
    local stack=CreateFrame("Frame",nil,holder);stack:SetSize(58,84);stack:SetPoint("CENTER",holder,"CENTER",0,0);stack:SetFrameLevel(holder:GetFrameLevel()+1);stack:EnableMouse(false)
    holder.stackAnchor=stack
    for i=1,3 do
        local layer=MakeCardBack(stack,p and p.cardBack or "classic",58,84,top)
        layer:SetPoint("CENTER",stack,"CENTER",(i-2)*1.5,-(i-2)*1.5)
        layer:SetFrameLevel(stack:GetFrameLevel()+i)
    end
    local count=Text(holder,tostring(PlayerDeckCount(p)),9.5,{1,1,1,1},FONT);count:SetPoint(top and "TOP" or "BOTTOM",0,top and -2 or 2)
    local lbl=CardText(holder,"DECK",6.5,P().muted,FONT);lbl:SetPoint(top and "BOTTOM" or "TOP",0,top and 2 or -2)
    return holder
end

local function CreateOpponentHand(parent,p)
    local count=math.max(0,math.min(HS.MAX_HAND or 7,tonumber(p and p.handCount) or #(p and p.hand or {})))
    if count<=0 then return nil end
    local holder=Dynamic(CreateFrame("Frame",nil,parent));holder:SetSize(250,66);holder:SetPoint("TOPRIGHT",parent,"TOPRIGHT",-142,-12);holder:SetFrameLevel(parent:GetFrameLevel()+8);holder:EnableMouse(false)
    local spacing=math.min(31,194/math.max(1,count-1));local start=-(count-1)*spacing
    for i=1,count do
        local back=MakeCardBack(holder,p and p.cardBack or "classic",40,58,true);back:SetPoint("TOPRIGHT",holder,"TOPRIGHT",start+(i-1)*spacing,0);back:SetFrameLevel(holder:GetFrameLevel()+i)
        if back.SetRotation then back:SetRotation(0) end
    end
    local label=CardText(holder,tostring(count).." IN HAND",6.8,P().muted,FONT);label:SetPoint("TOPRIGHT",-2,-59)
    return holder
end

local function MakeArenaButton(parent,label,w,h,accent)
    local b=Dynamic(CreateFrame("Button",nil,parent,"BackdropTemplate"));b:SetSize(w,h);b._baseW=w;b._baseH=h
    b:SetBackdrop({bgFile=HS_LEATHER,tile=true,tileSize=64,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=9,insets={left=3,right=3,top=3,bottom=3}});b:SetBackdropColor(.19,.095,.040,.995);b:SetBackdropBorderColor(.80,.52,.20,.96)
    local inner=b:CreateTexture(nil,"BACKGROUND");inner:SetTexture(WHITE);inner:SetPoint("TOPLEFT",5,-5);inner:SetPoint("BOTTOMRIGHT",-5,5);inner:SetColorTexture(.28,.14,.055,.58)
    local line=b:CreateTexture(nil,"BORDER");line:SetTexture(WHITE);line:SetPoint("TOPLEFT",10,-6);line:SetPoint("TOPRIGHT",-10,-6);line:SetHeight(2);line:SetColorTexture(1,.72,.28,.48)
    local ornL=b:CreateTexture(nil,"OVERLAY");ornL:SetTexture(ORNAMENT_ROYAL);ornL:SetSize(h*1.12,h*1.12);ornL:SetPoint("LEFT",-5,0);ornL:SetVertexColor(1,.72,.30,.26)
    local ornR=b:CreateTexture(nil,"OVERLAY");ornR:SetTexture(ORNAMENT_ROYAL);ornR:SetSize(h*1.12,h*1.12);ornR:SetPoint("RIGHT",5,0);ornR:SetVertexColor(1,.72,.30,.26);if ornR.SetRotation then ornR:SetRotation(3.1415926) end
    b.label=CardText(b,label,9.8,accent and {1,.86,.50,1} or P().pale,FONT);b.label:SetPoint("CENTER")
    b:SetScript("OnEnter",function(self)if self:IsEnabled() then self:SetScale(1.035);self:SetBackdropBorderColor(1,.72,.28,1) end end);b:SetScript("OnLeave",function(self)self:SetScale(1);self:SetBackdropBorderColor(.80,.52,.20,.96)end)
    b.SetTableEnabled=function(self,enabled)
        self:SetEnabled(enabled);self:SetAlpha(enabled and 1 or .48);self.label:SetTextColor(enabled and 1 or .58,enabled and .86 or .58,enabled and .50 or .58,1)
    end
    return b
end

local function PlayTurnButtonFlip(button,callback)
    if not button or button._flipping then return end
    button._flipping=true;button:SetEnabled(false)
    local start=GetTime and GetTime() or 0;local duration=.50;local baseW=button._baseW or button:GetWidth();local baseH=button._baseH or button:GetHeight()
    button:SetScript("OnUpdate",function(self)
        local now=GetTime and GetTime() or (start+duration);local t=math.min(1,(now-start)/duration);local phase=t*3.1415926;local wave=math.sin(phase);local depth=math.abs(math.cos(phase))
        -- Collapse vertically, not horizontally: this reads as the top edge of
        -- the button rotating away from the player around its X axis.
        local h=math.max(7,baseH*(.10+.90*depth));local w=baseW*(1-.055*wave)
        self:SetSize(w,h);self:SetScale(1+.025*wave);self:SetAlpha(.66+.34*depth);self.label:SetAlpha(.18+.82*depth)
        if t>=1 then self:SetScript("OnUpdate",nil);self:SetSize(baseW,baseH);self:SetScale(1);self:SetAlpha(1);self.label:SetAlpha(1);self._flipping=nil;if callback then callback() end end
    end)
end

local function ShowRageQuitBanner(f,match,viewerSide)
    if not f or f._rageBannerShownFor==match.id then return end
    f._rageBannerShownFor=match.id
    local loserSide=(match.winner==1 and 2) or (match.winner==2 and 1) or nil
    local loser=loserSide and match.players[loserSide]
    local overlay=Dynamic(CreateFrame("Frame",nil,f,"BackdropTemplate"));overlay:SetAllPoints();overlay:SetFrameLevel(f:GetFrameLevel()+260);overlay:EnableMouse(false);overlay:SetBackdrop({bgFile=WHITE});overlay:SetBackdropColor(.12,0,.12,.58)
    local title=Text(overlay,"SKILL ISSUE LOL",48,{1,.42,1,1},DISPLAY);title:SetPoint("CENTER",0,32)
    local sub=Text(overlay,(loser and Short(loser.display or loser.name,40) or "A PLAYER").." RAGE QUIT",20,{1,.86,.48,1},DISPLAY);sub:SetPoint("TOP",title,"BOTTOM",0,-12)
    local glow=overlay:CreateTexture(nil,"BACKGROUND");glow:SetTexture(CELESTIAL.."starlight_glow.tga");glow:SetBlendMode("ADD");glow:SetSize(760,330);glow:SetPoint("CENTER");glow:SetVertexColor(1,.18,1,.52)
    local ag=overlay:CreateAnimationGroup();ag:SetLooping("BOUNCE");local a=ag:CreateAnimation("Alpha");a:SetFromAlpha(.42);a:SetToAlpha(1);a:SetDuration(.24);ag:Play()
    if C_Timer and C_Timer.After then C_Timer.After(2.4,function()if ag and ag.Stop then ag:Stop() end;if overlay then overlay:Hide();overlay:SetParent(nil) end end) end
end

local function OpenMatchEscapeMenu(f)
    if not f or f._matchMenu and f._matchMenu:IsShown() then return end
    local match=HS.GetActiveMatch and HS.GetActiveMatch();if not match or match.finished or match.paused or not f._viewerSide or f._viewerSide<=0 then return end
    local shade=CreateFrame("Frame",nil,f,"BackdropTemplate");f._matchMenu=shade;shade:SetAllPoints();shade:SetFrameLevel(f:GetFrameLevel()+210);shade:EnableMouse(true);shade:SetBackdrop({bgFile=WHITE});shade:SetBackdropColor(0,0,0,.58)
    local box=CreateFrame("Frame",nil,shade,"BackdropTemplate");box:SetSize(470,360);box:SetPoint("CENTER");Backdrop(box,P().gold,.995);box:SetBackdropColor(.10,.045,.024,.995)
    local h=Text(box,"MATCH MENU",24,P().gold,DISPLAY);h:SetPoint("TOP",0,-24)
    local sub=Text(box,"Pause safely for later, concede normally, or make a terrible life choice.",10,P().muted);sub:SetPoint("TOPLEFT",30,-62);sub:SetPoint("TOPRIGHT",-30,-62);sub:SetJustifyH("CENTER");sub:SetWordWrap(true)
    local pause=Btn(box,"Pause Match",280,46,true);pause:SetPoint("TOP",0,-112);pause:SetScript("OnClick",function()
        local ok,msg=HS.PauseMatch();ShowStatus(msg or (ok and "Match paused." or "Could not pause."))
        if ok then
            local me=(GetUnitName and GetUnitName("player",true)) or (UnitName and UnitName("player")) or ""
            if match.mode=="practice" or match.host==me then shade:Hide();f._matchMenu=nil end
        end
    end)
    local concede=Btn(box,"Concede",280,42,false);concede:SetPoint("TOP",pause,"BOTTOM",0,-12);concede:SetScript("OnClick",function()local confirm=Text(box,"Click Concede again to confirm the loss.",9.5,{1,.68,.48,1});confirm:SetPoint("TOP",concede,"BOTTOM",0,-6);concede:SetScript("OnClick",function()HS.Concede();shade:Hide();f._matchMenu=nil end)end)
    local rage=Btn(box,"RAGEQUIT",280,42,false);rage:SetPoint("TOP",concede,"BOTTOM",0,-48);rage.label:SetTextColor(1,.38,.92,1);rage:SetScript("OnClick",function()HS.RageQuit();shade:Hide();f._matchMenu=nil end)
    local back=Btn(box,"Return to Game",160,30,false);back:SetPoint("BOTTOM",0,18);back:SetScript("OnClick",function()shade:Hide();f._matchMenu=nil end)
end

local function EnsureMatchFrame()
    if matchFrame then return matchFrame end
    local f=CreateFrame("Frame","ODYN_HearthstoneMatchFrame",UIParent,"BackdropTemplate")
    f:SetSize(1280,820);f:SetPoint("CENTER");f:SetFrameStrata("FULLSCREEN_DIALOG");f:SetFrameLevel(5400);f:SetToplevel(true);f:SetClampedToScreen(true);f:EnableMouse(true)
    f:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=14,insets={left=4,right=4,top=4,bottom=4}})
    f:SetBackdropColor(.055,.026,.020,1);f:SetBackdropBorderColor(.78,.50,.22,.98)
    -- A live Hearthstone table owns ESC itself. UISpecialFrames used to hide
    -- the table first, then OnHide reopened it and showed the menu, causing a blink.
    f:EnableKeyboard(true)
    if f.SetPropagateKeyboardInput then f:SetPropagateKeyboardInput(true) end
    f:SetScript("OnKeyDown",function(self,key)
        if key=="ESCAPE" and self._matchOpen and not self._matchFinished and self._viewerSide~=nil then
            if self.SetPropagateKeyboardInput then self:SetPropagateKeyboardInput(false) end
            if self._viewerSide<=0 then
                -- Spectators do not own a match and therefore need no concede
                -- dialog: ESC is a clean Leave Spectate action.
                if HS.LeaveSpectate then HS.LeaveSpectate() end
                self._allowHide=true;self._matchOpen=false;self:Hide();self._allowHide=nil
            elseif self._matchMenu and self._matchMenu:IsShown() then
                self._matchMenu:Hide();self._matchMenu=nil
            else
                OpenMatchEscapeMenu(self)
            end
        else
            if self.SetPropagateKeyboardInput then self:SetPropagateKeyboardInput(true) end
        end
    end)
    f:SetScript("OnKeyUp",function(self)if self.SetPropagateKeyboardInput then self:SetPropagateKeyboardInput(true) end end)
    f:Hide()
    f:SetScript("OnHide",function(self)
        if self.playedSpotlight then self.playedSpotlight:Hide();self.playedSpotlight:SetAlpha(1) end
        if self._allowHide or not self._matchOpen or self._matchFinished or not self._viewerSide or self._viewerSide<=0 then return end
        local active=HS.GetActiveMatch and HS.GetActiveMatch()
        if active and active.id==self._matchID and not active.finished and not active.paused then
            if C_Timer and C_Timer.After then C_Timer.After(0,function()self:Show();self:Raise();OpenMatchEscapeMenu(self)end) else self:Show();OpenMatchEscapeMenu(self) end
        end
    end)

    -- Header: a compact brass tavern plaque, visually separate from the table.
    local header=f:CreateTexture(nil,"BACKGROUND");header:SetTexture(WHITE);header:SetPoint("TOPLEFT",8,-8);header:SetPoint("TOPRIGHT",-8,-8);header:SetHeight(48);header:SetColorTexture(.095,.036,.026,1)
    local headerGlow=f:CreateTexture(nil,"BACKGROUND",nil,1);headerGlow:SetTexture(CELESTIAL.."starlight_glow.tga");headerGlow:SetBlendMode("ADD");headerGlow:SetPoint("TOPLEFT",40,-5);headerGlow:SetPoint("TOPRIGHT",-40,-5);headerGlow:SetHeight(54);headerGlow:SetVertexColor(1,.55,.18,.045)
    local headerLine=f:CreateTexture(nil,"ARTWORK");headerLine:SetTexture(WHITE);headerLine:SetPoint("BOTTOMLEFT",18,-57);headerLine:SetPoint("BOTTOMRIGHT",-18,-57);headerLine:SetHeight(2);headerLine:SetColorTexture(.93,.61,.25,.62)
    f.title=Text(f,"UNOFFICIAL HEARTHSTONE TABLE",20,{1,.80,.40,1},DISPLAY);f.title:SetPoint("TOPLEFT",22,-15)
    f.status=Text(f,"",10,P().muted);f.status:SetPoint("TOP",0,-18)
    f.dragHint=Text(f,"Drag a card onto the lane to choose its position  •  Drag a ready creature onto an enemy to attack",9,{.80,.70,.58,1});f.dragHint:SetPoint("TOP",0,-37)
    local close=Btn(f,"×",34,28,false);close:SetPoint("TOPRIGHT",-15,-12);close.label:SetFont(DISPLAY,Scaled(18),"OUTLINE");close:SetScript("OnClick",function()
        if f._viewerSide and f._viewerSide<=0 then
            if HS.LeaveSpectate then HS.LeaveSpectate() end
            f._allowHide=true;f._matchOpen=false;f:Hide();f._allowHide=nil
            return
        end
        OpenMatchEscapeMenu(f)
    end)

    f.content=CreateFrame("Frame",nil,f);f.content:SetPoint("TOPLEFT",14,-62);f.content:SetPoint("BOTTOMRIGHT",-14,14)
    f.surface=CreateFrame("Frame",nil,f.content,"BackdropTemplate");f.surface:SetAllPoints();f.surface:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=12,insets={left=4,right=4,top=4,bottom=4}});f.surface:SetBackdropColor(.038,.022,.019,1);f.surface:SetBackdropBorderColor(.34,.22,.14,1);f.surface:EnableMouse(false)

    -- Physical carved shell.  The battlefield sits inside this frame instead of
    -- being a giant tinted rectangle covering the whole addon window.
    local shell=f.surface:CreateTexture(nil,"BACKGROUND");shell:SetTexture(HS_WOOD);shell:SetAllPoints();shell:SetVertexColor(.28,.18,.12,.86)
    local shellShade=f.surface:CreateTexture(nil,"BACKGROUND",nil,1);shellShade:SetTexture(WHITE);shellShade:SetPoint("TOPLEFT",8,-8);shellShade:SetPoint("BOTTOMRIGHT",-8,8);shellShade:SetColorTexture(.035,.018,.014,.64)
    local frameArt=f.surface:CreateTexture(nil,"BORDER");frameArt:SetTexture(DETAIL_FRAME);frameArt:SetPoint("TOPLEFT",4,-4);frameArt:SetPoint("BOTTOMRIGHT",-4,4);frameArt:SetVertexColor(.86,.58,.30,.36)

    -- Two arena rails are identity pieces; the board itself stays neutral.
    f.topRail=f.surface:CreateTexture(nil,"BACKGROUND",nil,2);f.topRail:SetTexture(WHITE);f.topRail:SetPoint("TOPLEFT",18,-18);f.topRail:SetPoint("TOPRIGHT",-18,-18);f.topRail:SetHeight(72);f.topRail:SetColorTexture(.19,.075,.045,1)
    f.topRailLip=f.surface:CreateTexture(nil,"ARTWORK",nil,4);f.topRailLip:SetTexture(WHITE);f.topRailLip:SetPoint("BOTTOMLEFT",f.topRail,24,-1);f.topRailLip:SetPoint("BOTTOMRIGHT",f.topRail,-24,-1);f.topRailLip:SetHeight(3);f.topRailLip:SetColorTexture(.96,.66,.27,.7)
    f.bottomRail=f.surface:CreateTexture(nil,"BACKGROUND",nil,2);f.bottomRail:SetTexture(WHITE);f.bottomRail:SetPoint("BOTTOMLEFT",18,18);f.bottomRail:SetPoint("BOTTOMRIGHT",-18,18);f.bottomRail:SetHeight(104);f.bottomRail:SetColorTexture(.19,.075,.045,1)
    f.bottomRailLip=f.surface:CreateTexture(nil,"ARTWORK",nil,4);f.bottomRailLip:SetTexture(WHITE);f.bottomRailLip:SetPoint("TOPLEFT",f.bottomRail,24,1);f.bottomRailLip:SetPoint("TOPRIGHT",f.bottomRail,-24,1);f.bottomRailLip:SetHeight(3);f.bottomRailLip:SetColorTexture(.96,.66,.27,.7)

    -- Neutral central walnut battlefield with only restrained inlays.
    f.playfield=CreateFrame("Frame",nil,f.surface,"BackdropTemplate");f.playfield:SetPoint("TOPLEFT",46,-82);f.playfield:SetPoint("BOTTOMRIGHT",-46,118);f.playfield:SetBackdrop({bgFile=HS_WOOD,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=10,insets={left=3,right=3,top=3,bottom=3}});f.playfield:SetBackdropColor(.23,.135,.075,1);f.playfield:SetBackdropBorderColor(.45,.29,.16,.92);f.playfield:EnableMouse(false)
    local fieldTone=f.playfield:CreateTexture(nil,"BACKGROUND",nil,1);fieldTone:SetTexture(WHITE);fieldTone:SetPoint("TOPLEFT",4,-4);fieldTone:SetPoint("BOTTOMRIGHT",-4,4);fieldTone:SetColorTexture(.14,.075,.038,.50)
    local fieldWarm=f.playfield:CreateTexture(nil,"BACKGROUND",nil,2);fieldWarm:SetTexture(CELESTIAL.."starlight_glow.tga");fieldWarm:SetBlendMode("ADD");fieldWarm:SetSize(740,410);fieldWarm:SetPoint("CENTER");fieldWarm:SetVertexColor(1,.50,.16,.035)

    -- Arena identity is split strictly into two horizontal halves.  The old
    -- anchors ended at CENTER, which accidentally produced diagonal quarters
    -- (top-left vs bottom-right) whenever players used different arenas.
    f.topArenaWash=f.playfield:CreateTexture(nil,"BACKGROUND",nil,3);f.topArenaWash:SetTexture(WHITE);f.topArenaWash:SetPoint("TOPLEFT",7,-7);f.topArenaWash:SetPoint("BOTTOMRIGHT",f.playfield,"RIGHT",-7,5);f.topArenaWash:SetColorTexture(.36,.22,.12,.18)
    f.bottomArenaWash=f.playfield:CreateTexture(nil,"BACKGROUND",nil,3);f.bottomArenaWash:SetTexture(WHITE);f.bottomArenaWash:SetPoint("TOPLEFT",f.playfield,"LEFT",7,-5);f.bottomArenaWash:SetPoint("BOTTOMRIGHT",-7,7);f.bottomArenaWash:SetColorTexture(.36,.22,.12,.18)
    f.topArenaPattern=f.playfield:CreateTexture(nil,"BACKGROUND",nil,4);f.topArenaPattern:SetPoint("TOPLEFT",8,-8);f.topArenaPattern:SetPoint("BOTTOMRIGHT",f.playfield,"RIGHT",-8,6);f.topArenaPattern:SetTexture(ArenaPattern(ArenaDef("tavern")));f.topArenaPattern:SetAlpha(.075)
    f.bottomArenaPattern=f.playfield:CreateTexture(nil,"BACKGROUND",nil,4);f.bottomArenaPattern:SetPoint("TOPLEFT",f.playfield,"LEFT",8,-6);f.bottomArenaPattern:SetPoint("BOTTOMRIGHT",-8,8);f.bottomArenaPattern:SetTexture(ArenaPattern(ArenaDef("tavern")));f.bottomArenaPattern:SetAlpha(.075);if f.bottomArenaPattern.SetRotation then f.bottomArenaPattern:SetRotation(3.1415926) end

    -- Accent edges now reinforce the horizontal ownership split instead of
    -- painting one entire left/right side of the table.
    f.topArenaWing=f.playfield:CreateTexture(nil,"ARTWORK",nil,1);f.topArenaWing:SetTexture(WHITE);f.topArenaWing:SetPoint("TOPLEFT",12,-10);f.topArenaWing:SetPoint("TOPRIGHT",-12,-10);f.topArenaWing:SetHeight(5);f.topArenaWing:SetColorTexture(1,.66,.27,.12)
    f.bottomArenaWing=f.playfield:CreateTexture(nil,"ARTWORK",nil,1);f.bottomArenaWing:SetTexture(WHITE);f.bottomArenaWing:SetPoint("BOTTOMLEFT",12,10);f.bottomArenaWing:SetPoint("BOTTOMRIGHT",-12,10);f.bottomArenaWing:SetHeight(5);f.bottomArenaWing:SetColorTexture(1,.66,.27,.12)
    f.topArenaTrim=f.playfield:CreateTexture(nil,"ARTWORK",nil,2);f.topArenaTrim:SetTexture(WHITE);f.topArenaTrim:SetPoint("TOPLEFT",30,-28);f.topArenaTrim:SetPoint("TOPRIGHT",-30,-28);f.topArenaTrim:SetHeight(1);f.topArenaTrim:SetColorTexture(1,.66,.27,.34)
    f.bottomArenaTrim=f.playfield:CreateTexture(nil,"ARTWORK",nil,2);f.bottomArenaTrim:SetTexture(WHITE);f.bottomArenaTrim:SetPoint("BOTTOMLEFT",30,28);f.bottomArenaTrim:SetPoint("BOTTOMRIGHT",-30,28);f.bottomArenaTrim:SetHeight(1);f.bottomArenaTrim:SetColorTexture(1,.66,.27,.34)

    f.topArenaGlow=f.playfield:CreateTexture(nil,"BACKGROUND",nil,5);f.topArenaGlow:SetTexture(CELESTIAL.."starlight_glow.tga");f.topArenaGlow:SetBlendMode("ADD");f.topArenaGlow:SetSize(650,170);f.topArenaGlow:SetPoint("TOP",0,-16);f.topArenaGlow:SetVertexColor(1,.66,.27,.075)
    f.bottomArenaGlow=f.playfield:CreateTexture(nil,"BACKGROUND",nil,5);f.bottomArenaGlow:SetTexture(CELESTIAL.."starlight_glow.tga");f.bottomArenaGlow:SetBlendMode("ADD");f.bottomArenaGlow:SetSize(650,170);f.bottomArenaGlow:SetPoint("BOTTOM",0,16);f.bottomArenaGlow:SetVertexColor(1,.66,.27,.075)
    f.topArenaCrest=f.playfield:CreateTexture(nil,"ARTWORK",nil,3);f.topArenaCrest:SetTexture(OrnamentForArena(ArenaDef("tavern"),true));f.topArenaCrest:SetSize(78,78);f.topArenaCrest:SetPoint("TOPLEFT",24,-18);f.topArenaCrest:SetVertexColor(1,.66,.27,.22)
    f.bottomArenaCrest=f.playfield:CreateTexture(nil,"ARTWORK",nil,3);f.bottomArenaCrest:SetTexture(OrnamentForArena(ArenaDef("tavern"),true));f.bottomArenaCrest:SetSize(78,78);f.bottomArenaCrest:SetPoint("BOTTOMRIGHT",-24,18);f.bottomArenaCrest:SetVertexColor(1,.66,.27,.22);if f.bottomArenaCrest.SetRotation then f.bottomArenaCrest:SetRotation(3.1415926) end
    f.topArenaName=Text(f.playfield,"TAVERN HEARTH",7.4,{1,.72,.34,.72},FONT);f.topArenaName:SetPoint("TOPLEFT",100,-18)
    f.bottomArenaName=Text(f.playfield,"TAVERN HEARTH",7.4,{1,.72,.34,.72},FONT);f.bottomArenaName:SetPoint("BOTTOMRIGHT",-100,18)

    -- The recessed lane plates are anchored to the exact creature-row centers.
    -- This keeps the rectangular backing directly behind both players' cards
    -- instead of drifting above/below them as the table shell changes.
    local function LanePlate(y)
        local lane=CreateFrame("Frame",nil,f.surface,"BackdropTemplate");lane:SetSize(924,226);lane:SetPoint("CENTER",f.content,"CENTER",0,y);lane:SetFrameLevel(f.surface:GetFrameLevel()+8);lane:EnableMouse(false)
        lane:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=9,insets={left=3,right=3,top=3,bottom=3}});lane:SetBackdropColor(.030,.017,.012,.42);lane:SetBackdropBorderColor(.47,.30,.16,.42)
        local inset=lane:CreateTexture(nil,"BACKGROUND");inset:SetTexture(HS_WOOD);inset:SetPoint("TOPLEFT",5,-5);inset:SetPoint("BOTTOMRIGHT",-5,5);inset:SetVertexColor(.20,.115,.065,.44)
        local inner=lane:CreateTexture(nil,"BACKGROUND",nil,1);inner:SetTexture(WHITE);inner:SetPoint("TOPLEFT",11,-11);inner:SetPoint("BOTTOMRIGHT",-11,11);inner:SetColorTexture(.015,.008,.006,.20)
        return lane
    end
    f.topLanePanel=LanePlate(128);f.bottomLanePanel=LanePlate(-80)
    local centerShadow=f.playfield:CreateTexture(nil,"ARTWORK",nil,2);centerShadow:SetTexture(WHITE);centerShadow:SetPoint("LEFT",52,8);centerShadow:SetPoint("RIGHT",-52,8);centerShadow:SetHeight(7);centerShadow:SetColorTexture(.025,.012,.008,.58)
    local centerLine=f.playfield:CreateTexture(nil,"ARTWORK",nil,3);centerLine:SetTexture(WHITE);centerLine:SetPoint("LEFT",58,8);centerLine:SetPoint("RIGHT",-58,8);centerLine:SetHeight(2);centerLine:SetColorTexture(.76,.46,.20,.62)
    local gem=f.playfield:CreateTexture(nil,"ARTWORK",nil,5);gem:SetTexture(CELESTIAL.."starlight_diamond.tga");gem:SetBlendMode("ADD");gem:SetSize(20,20);gem:SetPoint("CENTER",0,8);gem:SetVertexColor(1,.72,.30,.82)

    -- Deck alcoves are part of the carved edge, not floating black boxes.
    local function DeckWell(top)
        local well=CreateFrame("Frame",nil,f.surface,"BackdropTemplate");well:SetSize(94,120);well:SetFrameLevel(f.surface:GetFrameLevel()+6);well:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=9,insets={left=3,right=3,top=3,bottom=3}});well:SetBackdropColor(.028,.016,.014,.92);well:SetBackdropBorderColor(.82,.54,.26,.58);well:EnableMouse(false)
        if top then well:SetPoint("TOPRIGHT",-28,-92) else well:SetPoint("BOTTOMRIGHT",-28,126) end
        local inset=well:CreateTexture(nil,"BACKGROUND");inset:SetTexture(CELESTIAL.."starlight_round.tga");inset:SetBlendMode("ADD");inset:SetAllPoints();inset:SetVertexColor(1,.58,.20,.038)
        return well
    end
    f.topDeckWell=DeckWell(true);f.bottomDeckWell=DeckWell(false)

    -- Hero docks give each portrait a deliberate home and keep the lanes clear.
    local topDock=f.surface:CreateTexture(nil,"BORDER",nil,2);topDock:SetTexture(WHITE);topDock:SetSize(334,86);topDock:SetPoint("TOP",0,-10);topDock:SetColorTexture(.025,.014,.012,.76)
    local bottomDock=f.surface:CreateTexture(nil,"BORDER",nil,2);bottomDock:SetTexture(WHITE);bottomDock:SetSize(330,105);bottomDock:SetPoint("BOTTOMLEFT",15,15);bottomDock:SetColorTexture(.025,.014,.012,.78)

    -- Interaction layers.  Arena decoration can never sit above cards/HUD.
    f.boardLayer=CreateFrame("Frame",nil,f.content);f.boardLayer:SetAllPoints();f.boardLayer:SetFrameLevel(f.content:GetFrameLevel()+20)
    f.fxLayer=CreateFrame("Frame",nil,f.content);f.fxLayer:SetAllPoints();f.fxLayer:SetFrameLevel(f.content:GetFrameLevel()+34)
    f.handLayer=CreateFrame("Frame",nil,f.content);f.handLayer:SetAllPoints();f.handLayer:SetFrameLevel(f.content:GetFrameLevel()+48)
    f.hudLayer=CreateFrame("Frame",nil,f.content);f.hudLayer:SetAllPoints();f.hudLayer:SetFrameLevel(f.content:GetFrameLevel()+72)

    f.ownBoardDrop=CreateFrame("Frame",nil,f.boardLayer);f.ownBoardDrop:SetSize(924,226);f.ownBoardDrop:SetPoint("CENTER",f.content,"CENTER",0,-80);f.ownBoardDrop:EnableMouse(false)
    f.playDrop=CreateFrame("Frame",nil,f.boardLayer,"BackdropTemplate");f.playDrop:SetAllPoints(f.ownBoardDrop);f.playDrop:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=8,insets={left=2,right=2,top=2,bottom=2}});f.playDrop:SetBackdropColor(.75,.46,.13,.018);f.playDrop:SetBackdropBorderColor(1,.70,.25,.52);f.playDrop:SetAlpha(.06);f.playDrop:EnableMouse(false);f.playDrop.label=Text(f.playDrop,"DROP TO CHOOSE BOARD POSITION",8.8,{1,.82,.48,.82},DISPLAY);f.playDrop.label:SetPoint("CENTER",0,0)
    f.placeGuide=CreateFrame("Frame",nil,f.boardLayer,"BackdropTemplate");f.placeGuide:SetSize(144,202);f.placeGuide:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=10,insets={left=2,right=2,top=2,bottom=2}});f.placeGuide:SetBackdropColor(1,.64,.18,.028);f.placeGuide:SetBackdropBorderColor(1,.78,.30,.82);f.placeGuide:SetFrameLevel(f.boardLayer:GetFrameLevel()+4);f.placeGuide:EnableMouse(false);f.placeGuide:Hide()
    local pg=f.placeGuide:CreateTexture(nil,"OVERLAY");pg:SetTexture(CELESTIAL.."starlight_glow.tga");pg:SetBlendMode("ADD");pg:SetPoint("TOPLEFT",-12,12);pg:SetPoint("BOTTOMRIGHT",12,-12);pg:SetVertexColor(1,.67,.20,.13)

    f.boardSlots={{},{}}
    for row=1,2 do
        for i=1,5 do
            local slot=CreateMatchBoardSlot(f.boardLayer);slot._matchFrame=f
            slot:SetPoint("CENTER",f.content,"CENTER",CenteredBoardX(5,i),row==1 and 128 or -80)
            f.boardSlots[row][i]=slot
        end
    end
    f.playedSpotlight=CreatePlayedSpotlight(f.fxLayer,f)
    EnsureTurnBanner(f)
    matchFrame=f
    return f
end

local function RenderMatch(match,viewerSide)
    local f=EnsureMatchFrame()
    local newMatch=f._matchID~=match.id
    local introPieces={}
    if not newMatch and f.boardSlots then
        for row=1,2 do for i=1,(HS.MAX_BOARD or 5) do local slot=f.boardSlots[row] and f.boardSlots[row][i];if slot and slot._introDropping then FinalizeDropMatchPiece(slot,slot._introDropping,false) end end end
    end
    if newMatch then
        f._matchID=match.id;f._snapshot=nil;f._lastAnimatedActionSeq=0;f._lastPlayedSpotToken=nil;f._slotByUID={}
        if f._dragGhost then f._dragGhost:Hide();if f._dragGhost.driver then f._dragGhost.driver:Hide() end;f._dragGhost._matchFrame=nil end
        f._handDrag=nil;f._weaponDrag=nil;f._pendingMode=nil;f._pendingCard=nil;f._pendingPlayPosition=nil
        if f.playedSpotlight then f.playedSpotlight:Hide();f.playedSpotlight:SetAlpha(1) end
        if f.turnBanner then f.turnBanner._lastKey=nil;if f.turnBanner.anim and f.turnBanner.anim:IsPlaying() then f.turnBanner.anim:Stop() end;f.turnBanner:Hide() end
    end
    -- Defensive cleanup for the rare "floating duplicate card" case. A drag
    -- ghost is only legitimate while _handDrag is live; if a start/state sync
    -- interrupted a drag, never let that UIParent-level ghost survive.
    if f._dragGhost and not f._handDrag then
        f._dragGhost:Hide();if f._dragGhost.driver then f._dragGhost.driver:Hide() end;f._dragGhost._matchFrame=nil
    end
    local previous=f._snapshot;local mine,opp,me=MatchView(match,viewerSide);local spectator=me==0;f._viewerSide=me;f._matchOpen=true;f._matchFinished=match.finished and true or false
    local incomingSnapshot=SnapshotMatch(match)
    local ownVisual=mine or match.players[2];ApplyArenaHalves(f,opp,ownVisual)
    ClearTargetHints(f);if f.emoteMenu then f.emoteMenu:Hide();f.emoteMenu=nil end;Clear(f.content);Clear(f.handLayer);Clear(f.hudLayer);Clear(f.fxLayer);f._deadTargets={}
    -- Preserve the old visual just long enough to dissolve any creature that
    -- disappeared from the authoritative board state before slots are reused.
    if previous and f._slotByUID then
        for side=1,2 do
            local oldBoard=(previous.players[side] and previous.players[side].board) or {}
            local newBoard=(incomingSnapshot.players[side] and incomingSnapshot.players[side].board) or {}
            for uid in pairs(oldBoard) do
                if not newBoard[uid] then local key=tostring(side)..":"..tostring(uid);local oldSlot=f._slotByUID[key];if oldSlot then PlayDamageBurst(oldSlot);f._deadTargets[key]=PlayCardDeath(f,oldSlot) or oldSlot end end
            end
        end
    end
    f._targetFrames={};f._frameByKey={};f._slotByUID={};f._heroByName={};f.status:SetText(match.finished and "MATCH COMPLETE" or (match.paused and "MATCH PAUSED" or (spectator and "SPECTATING" or ((match.turnSide==me) and "YOUR TURN" or "OPPONENT'S TURN"))))
    local function ResolvePending(kind,uid)
        if f._pendingMode=="target" then
            local wanted=f._pendingCard and f._pendingCard.target
            if not IsPendingTargetValid(wanted,kind) then
                ShowStatus("That is not a valid target for "..tostring(f._pendingCard and f._pendingCard.name or "this ability")..".")
                return true
            end
            local targetSpec=TargetSpec(kind,uid)
            local ok,err
            if f._pendingAction=="ability" then
                -- Hero abilities consume their target from ProcessAction's `a`
                -- argument. Card plays use `a` for the hand index and therefore
                -- carry their target in `b`. Keeping these routes separate avoids
                -- targeted hero powers arriving at the engine with an empty target.
                ok,err=HS.LocalAction("ability",targetSpec)
            else
                local payload=targetSpec
                if f._pendingAction=="play" and f._pendingPlayPosition then payload="P"..tostring(f._pendingPlayPosition)..";"..targetSpec end
                ok,err=HS.LocalAction(f._pendingAction,f._pendingA,payload)
            end
            if not ok then
                if err then ShowStatus(err) end
                return true
            end
            f._pendingMode=nil;f._pendingAction=nil;f._pendingA=nil;f._pendingCard=nil;f._pendingPlayPosition=nil;ClearTargetHints(f);return true
        elseif f._pendingMode=="heroattack" and (kind=="enemyHero" or kind=="enemyCreature") then local ok,err=HS.LocalAction("heroattack",TargetSpec(kind,uid));if not ok and err then ShowStatus(err) end;f._pendingMode=nil;ClearTargetHints(f);return true end
    end
    local EMOTES={{"Thanks","THANKS"},{"Wow","WOW"},{"Well Played","WELL_PLAYED"},{"Oops","OOPS"},{"Greetings","GREETINGS"},{"Threaten","THREATEN"}}
    local function CloseEmoteMenu() if f.emoteMenu then f.emoteMenu:Hide();f.emoteMenu:SetParent(nil);f.emoteMenu=nil end end
    local function ShowSpeechBubble(hero,textValue)
        if not hero or not hero:IsShown() then return end
        if hero._speech then hero._speech:Hide();hero._speech:SetParent(nil) end
        local b=Dynamic(CreateFrame("Frame",nil,f.hudLayer,"BackdropTemplate"));hero._speech=b;b:SetSize(218,62);b:SetPoint("BOTTOM",hero,"TOP",0,9);b:SetFrameLevel(f.hudLayer:GetFrameLevel()+45);b:SetBackdrop({bgFile=HS_PARCHMENT,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=8,insets={left=4,right=4,top=4,bottom=4}});b:SetBackdropColor(.94,.86,.69,.99);b:SetBackdropBorderColor(.36,.23,.12,.95);b:EnableMouse(false)
        local t=CardText(b,textValue,11.5,{.12,.065,.025,1},FONT);t:SetPoint("CENTER");t:SetWidth(192);t:SetHeight(46);t:SetJustifyH("CENTER");t:SetJustifyV("MIDDLE");t:SetWordWrap(true)
        local tail=b:CreateTexture(nil,"ARTWORK");tail:SetTexture(WHITE);tail:SetSize(14,10);tail:SetPoint("TOP",hero,"TOP",0,4);tail:SetColorTexture(.86,.72,.50,.96);if tail.SetRotation then tail:SetRotation(.785398) end
        b:SetAlpha(0);b:Show();local ag=b:CreateAnimationGroup();local fi=ag:CreateAnimation("Alpha");fi:SetOrder(1);fi:SetFromAlpha(0);fi:SetToAlpha(1);fi:SetDuration(.12);local hold=ag:CreateAnimation("Alpha");hold:SetOrder(2);hold:SetFromAlpha(1);hold:SetToAlpha(1);hold:SetDuration(1.45);local fo=ag:CreateAnimation("Alpha");fo:SetOrder(3);fo:SetFromAlpha(1);fo:SetToAlpha(0);fo:SetDuration(.22);ag:SetScript("OnFinished",function()if b then b:Hide();b:SetParent(nil) end;if hero then hero._speech=nil end end);ag:Play()
    end
    f._showSpeechBubble=function(sender,key)
        local labels={THANKS="Thanks",WOW="Wow",WELL_PLAYED="Well Played",OOPS="Oops",GREETINGS="Greetings",THREATEN="Threaten"}
        local hero=f._heroByName and f._heroByName[sender];if hero then ShowSpeechBubble(hero,(f._incomingEmoteText and f._incomingEmoteText~="") and f._incomingEmoteText or labels[key] or key) end
    end
    local function OpenEmoteMenu(hero,p)
        CloseEmoteMenu()
        local m=Dynamic(CreateFrame("Frame",nil,f.hudLayer));f.emoteMenu=m;m:SetSize(410,154);m:SetPoint("CENTER",hero,"CENTER",hero._isMine and 116 or 0,0);m:SetFrameLevel(f.hudLayer:GetFrameLevel()+50);m:EnableMouse(false)
        local positions={{-148,52},{148,52},{-170,0},{170,0},{-148,-52},{148,-52}}
        for i,e in ipairs(EMOTES) do local label,key=e[1],e[2];local b=CreateFrame("Button",nil,m,"BackdropTemplate");b:SetSize(132,38);b:SetPoint("CENTER",positions[i][1],positions[i][2]);b:EnableMouse(true);b:SetBackdrop({bgFile=HS_PARCHMENT,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=8,insets={left=4,right=4,top=4,bottom=4}});b:SetBackdropColor(.94,.86,.70,.99);b:SetBackdropBorderColor(.36,.23,.12,.96);local tx=CardText(b,label,10.5,{.13,.07,.025,1},FONT);tx:SetPoint("CENTER");b:SetScript("OnEnter",function(self)self:SetBackdropBorderColor(P().gold[1],P().gold[2],P().gold[3],1);self:SetScale(1.04) end);b:SetScript("OnLeave",function(self)self:SetBackdropBorderColor(.36,.23,.12,.96);self:SetScale(1) end);b:SetScript("OnClick",function()local phrase=(HS.GetEmoteText and HS.GetEmoteText(key)) or label;ShowSpeechBubble(hero,phrase);if HS.SendEmote then HS.SendEmote(key,phrase) end;CloseEmoteMenu() end) end
    end

    f._weaponAttackToken=nil
    local function HeroPanel(p,top,isMine,sideIndex)
        local accent=AccentFromHex(p.accent)
        -- Keep equipped weapons physically inside the owner HUD instead of
        -- spawning a detached companion frame beside it.  The HUD widens only
        -- while a weapon is equipped, keeping the normal weaponless layout compact.
        local hasWeapon=p.weapon~=nil
        local h=Dynamic(CreateFrame("Button",nil,f.hudLayer,"BackdropTemplate"));h:SetSize(hasWeapon and 366 or 286,98);if top then h:SetPoint("TOP",f.content,"TOP",0,-4) else h:SetPoint("BOTTOMLEFT",f.content,"BOTTOMLEFT",20,22) end;h:SetClipsChildren(false);h:SetFrameLevel(f.hudLayer:GetFrameLevel()+5);h._isMine=isMine
        h:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=9,insets={left=2,right=2,top=2,bottom=2}});h:SetBackdropColor(.105,.055,.045,.985);h:SetBackdropBorderColor(accent[1],accent[2],accent[3],.82)
        local portraitBox=CreateFrame("Frame",nil,h,"BackdropTemplate");portraitBox:SetSize(76,76);portraitBox:SetPoint("LEFT",9,0);portraitBox:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=7,insets={left=2,right=2,top=2,bottom=2}});portraitBox:SetBackdropColor(.055,.028,.025,.99);portraitBox:SetBackdropBorderColor(accent[1],accent[2],accent[3],.94)
        local portrait=portraitBox:CreateTexture(nil,"ARTWORK");portrait:SetPoint("TOPLEFT",3,-3);portrait:SetPoint("BOTTOMRIGHT",-3,3);local unit=FindUnitByName(p.name);if unit and SetPortraitTexture then pcall(SetPortraitTexture,portrait,unit) else portrait:SetTexture("Interface\\Icons\\INV_Misc_Coin_01");portrait:SetTexCoord(.08,.92,.08,.92) end
        if RPAL.RenderCombatProfileAura then pcall(RPAL.RenderCombatProfileAura,portraitBox,portraitBox,p.aura or "None",accent,94) end

        local hp=CreateFrame("Frame",nil,h,"BackdropTemplate");hp:SetSize(108,31);hp:SetPoint("TOPLEFT",94,-14);hp:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=6,insets={left=2,right=2,top=2,bottom=2}});hp:SetBackdropColor(.34,.07,.055,.99);hp:SetBackdropBorderColor(.90,.32,.21,.96)
        local hpText=DText(hp,tostring(p.hp or 0)..(((p.armor or 0)>0) and ("  +"..tostring(p.armor).."A") or "").." HP",12.5,{1,1,1,1},FONT);hpText:SetPoint("CENTER")
        local mana=CreateFrame("Frame",nil,h,"BackdropTemplate");mana:SetSize(108,31);mana:SetPoint("BOTTOMLEFT",94,14);mana:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=6,insets={left=2,right=2,top=2,bottom=2}});mana:SetBackdropColor(.045,.16,.36,.99);mana:SetBackdropBorderColor(.35,.70,1,.96)
        local manaText=DText(mana,tostring(p.mana or 0).." / "..tostring(p.maxMana or 0).." MANA",12.5,{1,1,1,1},FONT);manaText:SetPoint("CENTER")

        local ability=HS.ABILITY_BY_ID[p.ability]
        local skill=CreateFrame("Button",nil,h);skill:SetSize(66,66);skill:SetPoint("RIGHT",hasWeapon and -82 or -7,0);skill:SetClipsChildren(false);skill:SetFrameLevel(h:GetFrameLevel()+20)
        skill.visual=CreateFrame("Frame",nil,skill);skill.visual:SetPoint("CENTER");skill.visual:SetSize(66,66);skill.visual:SetFrameLevel(skill:GetFrameLevel()+1);skill.visual:EnableMouse(false)
        local skillBg=skill.visual:CreateTexture(nil,"BACKGROUND");skillBg:SetTexture(WHITE);skillBg:SetSize(50,50);skillBg:SetPoint("CENTER");skillBg:SetColorTexture(.055,.028,.020,.98)
        local icon=skill.visual:CreateTexture(nil,"ARTWORK");icon:SetSize(48,48);icon:SetPoint("CENTER");icon:SetTexture(ability and ability.icon or "Interface\\Icons\\INV_Misc_QuestionMark");icon:SetTexCoord(.08,.92,.08,.92)
        if skill.visual.CreateMaskTexture and icon.AddMaskTexture then
            local mask=skill.visual:CreateMaskTexture();mask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask");mask:SetSize(48,48);mask:SetPoint("CENTER");icon:AddMaskTexture(mask);if skillBg.AddMaskTexture then skillBg:AddMaskTexture(mask) end;skill.mask=mask
        end
        local outer=skill.visual:CreateTexture(nil,"OVERLAY");outer:SetTexture("Interface\\Buttons\\UI-ActionButton-Border");outer:SetBlendMode("ADD");outer:SetSize(68,68);outer:SetPoint("CENTER");outer:SetVertexColor(P().gold[1],P().gold[2],P().gold[3],.88)
        local inner=skill.visual:CreateTexture(nil,"OVERLAY");inner:SetTexture("Interface\\Buttons\\UI-ActionButton-Border");inner:SetBlendMode("ADD");inner:SetSize(57,57);inner:SetPoint("CENTER");inner:SetVertexColor(.55,.80,1,.28)
        local manaHalo=skill.visual:CreateTexture(nil,"OVERLAY");manaHalo:SetTexture(CELESTIAL.."starlight_round.tga");manaHalo:SetBlendMode("ADD");manaHalo:SetSize(29,29);manaHalo:SetPoint("TOPLEFT",-3,4);manaHalo:SetVertexColor(.20,.58,1,.70)
        local cost=Text(skill.visual,tostring(ability and ability.cost or 0),12.5,{1,1,1,1},FONT);cost:SetPoint("CENTER",manaHalo,"CENTER",0,0)

        local function SetSkillHover(active)
            if active then
                skill.visual:SetScale(1.065);outer:SetAlpha(1)
                if not skill._tilt then skill._tilt=CreateFrame("Frame",nil,skill) end
                skill._tilt:Show();skill._tilt:SetScript("OnUpdate",function()
                    if not skill:IsMouseOver() then return end
                    local l,r,b,t=skill:GetLeft(),skill:GetRight(),skill:GetBottom(),skill:GetTop();if not l or not r or not b or not t then return end
                    local x,y=GetCursorPosition();local scale=UIParent:GetEffectiveScale() or 1;x=x/scale;y=y/scale
                    local dx=math.max(-1,math.min(1,(x-(l+r)*.5)/math.max(1,(r-l)*.5)));local dy=math.max(-1,math.min(1,(y-(b+t)*.5)/math.max(1,(t-b)*.5)))
                    skill.visual:ClearAllPoints();skill.visual:SetPoint("CENTER",skill,"CENTER",dx*2.5,dy*2.0)
                end)
            else
                if skill._tilt then skill._tilt:SetScript("OnUpdate",nil);skill._tilt:Hide() end
                skill.visual:SetScale(1);skill.visual:ClearAllPoints();skill.visual:SetPoint("CENTER",skill,"CENTER",0,0);outer:SetAlpha(.88)
            end
        end
        skill:SetScript("OnEnter",function(self)
            SetSkillHover(true)
            if GameTooltip then GameTooltip:SetOwner(self,"ANCHOR_RIGHT");GameTooltip:SetText(ability and ability.name or "Hero Skill",1,.82,.47);if ability then GameTooltip:AddLine(tostring(ability.cost or 0).." Mana",.55,.82,1);GameTooltip:AddLine(ability.text or "",1,1,1,true) end;GameTooltip:Show() end
        end)
        skill:SetScript("OnLeave",function()SetSkillHover(false);if GameTooltip then GameTooltip:Hide() end end)
        if isMine and not spectator and not match.finished and not match.paused then skill:SetScript("OnClick",function()if not ability or p.abilityUsed then ShowStatus("Hero Ability already used.");return end;if (p.mana or 0)<(ability.cost or 0) then ShowStatus("Not enough Mana.");return end;if ability.target then BeginPendingTarget(f,"ability","",ability) else local ok,err=HS.LocalAction("ability","");if not ok and err then ShowStatus(err) end end end) end

        -- Equipped weapon is a real section of the hero HUD.  This prevents
        -- it from floating over the arena/opponent hand and makes ownership
        -- immediately obvious for both players.
        if p.weapon then
            local weapon=p.weapon;local weaponCard=HS.GetCard(weapon.card)
            local divider=h:CreateTexture(nil,"ARTWORK");divider:SetTexture(WHITE);divider:SetSize(1,76);divider:SetPoint("RIGHT",h,"RIGHT",-76,0);divider:SetColorTexture(accent[1],accent[2],accent[3],.34)
            local weaponWrap=CreateFrame("Frame",nil,h,"BackdropTemplate");weaponWrap:SetSize(72,88);weaponWrap:SetPoint("RIGHT",h,"RIGHT",-3,0);weaponWrap:SetFrameLevel(h:GetFrameLevel()+18);weaponWrap:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=6,insets={left=2,right=2,top=2,bottom=2}});weaponWrap:SetBackdropColor(.042,.021,.016,.72);weaponWrap:SetBackdropBorderColor(accent[1],accent[2],accent[3],.38);weaponWrap:EnableMouse(false)
            local wl=Text(weaponWrap,"WEAPON",6.7,P().muted,DISPLAY);wl:SetPoint("TOP",0,-4)
            local wa=CreateFrame("Button",nil,weaponWrap,"BackdropTemplate");wa:SetSize(60,60);wa:SetPoint("BOTTOM",0,5);wa:SetFrameLevel(weaponWrap:GetFrameLevel()+3);wa:SetBackdrop({bgFile=WHITE,edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=7,insets={left=2,right=2,top=2,bottom=2}});wa:SetBackdropColor(.055,.026,.016,.99);local wc=weaponCard and CardColor(weaponCard) or P().gold;wa:SetBackdropBorderColor(wc[1],wc[2],wc[3],.98)
            local wi=wa:CreateTexture(nil,"ARTWORK");wi:SetPoint("TOPLEFT",5,-5);wi:SetPoint("BOTTOMRIGHT",-5,5);wi:SetTexture((weaponCard and (weaponCard.icon or weaponCard.art)) or "Interface\\Icons\\INV_Sword_04");wi:SetTexCoord(.08,.92,.08,.92)
            local atk=Text(wa,tostring(weapon.attack or 0),11.5,{1,1,1,1},FONT);atk:SetPoint("BOTTOMLEFT",4,3);local dur=Text(wa,tostring(weapon.durability or 0),11.5,{1,1,1,1},FONT);dur:SetPoint("BOTTOMRIGHT",-4,3)
            local ready=isMine and (not p.heroAttacked and me==match.turnSide and not match.finished and not match.paused)
            wa:SetAlpha((not isMine or ready) and 1 or .52)
            if isMine then
                wa:RegisterForDrag("LeftButton")
                wa:SetScript("OnDragStart",function()if ready then StartWeaponDrag(f,weapon,me,match) end end)
                wa:SetScript("OnDragStop",function()FinishWeaponDrag(f)end)
                wa:SetScript("OnClick",function()if not ready then ShowStatus("Your weapon cannot attack right now.");return end;f._pendingMode="heroattack";ShowStatus("Choose an enemy target for "..tostring(weaponCard and weaponCard.name or "your weapon")..".");for _,t in ipairs(f._targetFrames) do if t._hsTargetKind=="enemyHero" or t._hsTargetKind=="enemyCreature" then if t._hsTargetHint then t._hsTargetHint:SetAlpha(.68) end end end end)
                f._weaponAttackToken=wa
            end
            wa:SetScript("OnEnter",function(self)if GameTooltip then GameTooltip:SetOwner(self,isMine and "ANCHOR_RIGHT" or "ANCHOR_LEFT");GameTooltip:SetText(weaponCard and weaponCard.name or "Equipped Weapon",1,.82,.47);GameTooltip:AddLine(tostring(weapon.attack or 0).." Attack  ·  "..tostring(weapon.durability or 0).." Durability",1,1,1);if isMine then GameTooltip:AddLine(ready and "Drag this weapon onto an enemy to attack." or "Already used or unavailable this turn.",.75,.75,.75,true) else GameTooltip:AddLine("Opponent's equipped weapon.",.75,.75,.75,true) end;GameTooltip:Show() end end)
            wa:SetScript("OnLeave",function()if GameTooltip then GameTooltip:Hide() end end)
        end

        local hint=h:CreateTexture(nil,"OVERLAY");hint:SetTexture("Interface\\Buttons\\UI-ActionButton-Border");hint:SetBlendMode("ADD");hint:SetPoint("TOPLEFT",-8,8);hint:SetPoint("BOTTOMRIGHT",8,-8);hint:SetVertexColor(1,.66,.16,1);hint:SetAlpha(0);h._hsTargetHint=hint;h._hsTargetKind=isMine and "friendlyHero" or "enemyHero";table.insert(f._targetFrames,h);f._frameByKey["hero:"..tostring(sideIndex)]=h;f._heroByName[p.name]=h
        if not spectator and not match.finished and not match.paused then h:SetScript("OnClick",function()if f._pendingMode then if me~=match.turnSide then return end;if ResolvePending(h._hsTargetKind) then return end elseif isMine then OpenEmoteMenu(h,p) end end) end
        if newMatch then introPieces[#introPieces+1]=h end
        return h
    end
    local oppSide=(me==1 and 2) or (me==2 and 1) or 1;HeroPanel(opp,true,false,oppSide);if mine then HeroPanel(mine,false,true,me) else HeroPanel(match.players[2],false,false,2) end
    local topDeck=CreateDeckPile(f.hudLayer,opp,true,f.topDeckWell);local bottomDeck=CreateDeckPile(f.hudLayer,mine or match.players[2],false,f.bottomDeckWell);f.topDeckPile=topDeck;f.bottomDeckPile=bottomDeck;local oppHand=CreateOpponentHand(f.handLayer,opp);f.opponentHand=oppHand;if newMatch then introPieces[#introPieces+1]=topDeck;introPieces[#introPieces+1]=bottomDeck;if oppHand then introPieces[#introPieces+1]=oppHand end end
    f._ownOrderedSlots={}
    local function Board(p,row,isMine,sideIndex)
        local count=math.min((HS.MAX_BOARD or 5),#(p.board or {}));local used=0
        for i,u in ipairs(p.board or {}) do if i>(HS.MAX_BOARD or 5) then break end;used=i;local card=HS.GetCard(u.card);local uRef=u;local mineRef=isMine;local slot=f.boardSlots[row][i];slot._spectator=spectator;slot._turnSide=match.turnSide;slot._viewerSide=me
            if slot._introDropping then FinalizeDropMatchPiece(slot,slot._introDropping,false) end
            local boardX=CenteredBoardX(math.max(1,count),i);slot._actualX=boardX;slot._previewX=boardX;slot:ClearAllPoints();slot:SetPoint("CENTER",f.content,"CENTER",boardX,row==1 and 128 or -80);if isMine then f._ownOrderedSlots[#f._ownOrderedSlots+1]=slot end
            local function clickUnit()if spectator or match.finished or me~=match.turnSide then return end;if f._pendingMode=="target" then ResolvePending(mineRef and "friendlyCreature" or "enemyCreature",uRef.uid) elseif f._pendingMode=="heroattack" and not mineRef then ResolvePending("enemyCreature",uRef.uid) elseif mineRef and uRef.canAttack then ShowStatus("Drag "..card.name.." onto an enemy to attack.") end end
            UpdateMatchBoardSlot(slot,uRef,card,mineRef,clickUnit);slot._boardIndex=i;slot._boardRow=row;slot._boardSide=sideIndex;table.insert(f._targetFrames,slot);f._frameByKey["unit:"..tostring(sideIndex)..":"..tostring(u.uid)]=slot;f._slotByUID[tostring(sideIndex)..":"..tostring(u.uid)]=slot;if newMatch then introPieces[#introPieces+1]=slot end
        end
        for i=used+1,(HS.MAX_BOARD or 5) do if f.boardSlots[row][i]:IsShown() then HideMatchBoardSlot(f.boardSlots[row][i]) end end
    end
    f._mineBoardCount=mine and math.max(1,math.min((HS.MAX_BOARD or 5),#(mine.board or {}))) or 1
    Board(opp,1,false,oppSide);if mine then Board(mine,2,true,me) else Board(match.players[2],2,false,2) end;ShowPlayedSpotlight(f,match)
    -- Deployment: only the genuinely NEW card gets its drop-in. Existing arena
    -- cards never bounce when another card is deployed or when state refreshes.
    if previous and not newMatch and previous.players then
        for side=1,2 do
            local prevBoard=(previous.players[side] and previous.players[side].board) or {}
            local previousUIDs={}
            -- SnapshotMatch stores board units by UID, not as an array. Using
            -- ipairs here made the previous board look empty on every refresh,
            -- so every existing card replayed the deploy drop each turn.
            for oldUID in pairs(prevBoard) do previousUIDs[tostring(oldUID)]=true end
            local nowPlayer=match.players[side]
            for _,unit in ipairs((nowPlayer and nowPlayer.board) or {}) do
                local uid=tostring(unit.uid or "")
                if uid~="" and not previousUIDs[uid] then
                    local piece=f._frameByKey["unit:"..side..":"..uid]
                    if piece then DropMatchPiece(f,piece,155,0) end
                end
            end
        end
    end
    if mine and not spectator then
        -- Hand cards use the complete card layout.  They overlap slightly like a
        -- physical hand, while hover still opens the larger inspection + keyword panel.
        local hand=mine.hand or {};local spacing=math.min(96,math.max(80,690/math.max(1,#hand)));local start=-(#hand-1)*spacing*.5;for i,id in ipairs(hand) do local card=HS.GetCard(id);local c=MakeCard(f.handLayer,card,122,171,false);c:SetPoint("BOTTOM",start+(i-1)*spacing,4);c._hsPlayable=(me==match.turnSide and not match.finished and not match.paused and (mine.mana or 0)>=(card.cost or 0) and (card.type~="Creature" or #(mine.board or {})<(HS.MAX_BOARD or 5)));c:RegisterForDrag("LeftButton");c:SetScript("OnClick",function()ShowStatus(c._hsPlayable and (card.target and ("Drag "..card.name.." directly onto a target, or onto your table to choose a target.") or ("Drag "..card.name.." onto the table to play it.")) or ((mine.mana or 0)<(card.cost or 0) and "Not enough Mana." or "That card cannot be played right now."))end);c:SetScript("OnDragStart",function()StartHandDrag(f,card,i,mine,me,match)end);c:SetScript("OnDragStop",function()FinishHandDrag(f)end);end
        -- Equipped weapons are rendered beside both hero panels above.
        local endTurn=MakeArenaButton(f.hudLayer,"END TURN",144,42,true);f.endTurnButton=endTurn;endTurn:SetPoint("RIGHT",f.content,"RIGHT",-9,8);endTurn:SetFrameLevel(f.hudLayer:GetFrameLevel()+25);endTurn:SetTableEnabled(me==match.turnSide and not match.finished and not match.paused);endTurn:SetScript("OnClick",function(self)if me~=match.turnSide or match.finished then return end;PlayTurnButtonFlip(self,function()local ok,err=HS.LocalAction("end");if not ok and err then ShowStatus(err);self:SetTableEnabled(true) end end)end);if newMatch then introPieces[#introPieces+1]=endTurn end
    else local info=DText(f.hudLayer,spectator and "Spectator view · hidden hands remain private." or "Waiting for match state...",9,P().muted);info:SetPoint("BOTTOM",0,18) end
    if match.finished then
        if tostring(match.finishReason or "")=="ragequit" then ShowRageQuitBanner(f,match,me) end
        local shade=Dynamic(CreateFrame("Frame",nil,f.content));shade:SetAllPoints();shade:SetFrameLevel(f.content:GetFrameLevel()+55);shade:EnableMouse(true);local st=shade:CreateTexture(nil,"BACKGROUND");st:SetAllPoints();st:SetTexture(WHITE);st:SetColorTexture(0,0,0,.58)
        local result=CreateFrame("Frame",nil,shade,"BackdropTemplate");result:SetSize(560,205);result:SetPoint("CENTER");Backdrop(result,P().gold,.995);result:SetBackdropColor(.09,.043,.017,.995);local win=match.winner and match.winner>0 and me==match.winner;local msg="DRAW";if match.winner and match.winner>0 then msg=win and "VICTORY" or (me>0 and "DEFEAT" or (Short(match.players[match.winner].display,30).." WON")) end
        local title=Text(result,msg,31,win and {0.45,1,.58,1} or P().gold,DISPLAY);title:SetPoint("TOP",0,-27);local duration=Text(result,"MATCH TIME  "..FormatMatchDuration(match.durationSec or 0),9,P().muted);duration:SetPoint("TOP",title,"BOTTOM",0,-10)
        local rewardText="";if match.mode=="practice" then rewardText="Practice matches do not award match currency." elseif win and (match.rewardTokens or 0)>0 then rewardText="+"..tostring(match.rewardTokens).." "..string.upper(HS.GetCurrencyName and HS.GetCurrencyName() or "Tokens") elseif me>0 and not win then rewardText="No victory "..tostring(HS.GetCurrencyName and HS.GetCurrencyName() or "Tokens").." awarded." else rewardText="Match complete." end
        local reward=Text(result,rewardText,12,win and {1,.80,.32,1} or P().pale,DISPLAY);reward:SetPoint("TOP",duration,"BOTTOM",0,-13);local reason=Text(result,string.upper(tostring(match.finishReason or "match")),7.5,P().muted);reason:SetPoint("TOP",reward,"BOTTOM",0,-8);local close=Btn(result,"Close Table",128,30,true);close:SetPoint("BOTTOM",0,18);close:SetScript("OnClick",function()f:Hide()end)
    end
    f:Show();f:Raise()
    if newMatch then PlayMatchEntrance(f,introPieces) end
    local current=incomingSnapshot
    local la=match.lastAction;local newAttack=la and (la.kind=="attack" or la.kind=="heroattack") and (tonumber(la.seq) or 0)>(tonumber(f._lastAnimatedActionSeq) or 0)
    local attackTargetSide=newAttack and ((tonumber(la.side)==1) and 2 or 1) or nil
    if previous then
        if previous.turnSide~=current.turnSide and not match.finished then ShowTurnBanner(f,me>0 and current.turnSide==me,tostring(match.id)..":"..tostring(match.turn or 0)..":"..tostring(current.turnSide)) end
        for side=1,2 do
            local op=previous.players[side] or {};local np=current.players[side] or {};local oldHero=(tonumber(op.hp) or 0)+(tonumber(op.armor) or 0);local newHero=(tonumber(np.hp) or 0)+(tonumber(np.armor) or 0);local heroFrame=f._frameByKey["hero:"..side]
            local delayedHeroImpact=newAttack and side==attackTargetSide and la.targetKind=="enemyHero"
            if heroFrame and newHero<oldHero and not delayedHeroImpact then PlayDamageBurst(heroFrame);PlayShake(heroFrame,7) end
            for uid,nu in pairs(np.board or {}) do local ou=op.board and op.board[uid];local unitFrame=f._frameByKey["unit:"..side..":"..uid];local delayedUnitImpact=newAttack and side==attackTargetSide and la.targetKind=="enemyCreature" and tonumber(uid)==tonumber(la.targetID);if unitFrame and ou and (tonumber(nu.hp) or 0)<(tonumber(ou.hp) or 0) and not delayedUnitImpact then PlayDamageBurst(unitFrame);PlayShake(unitFrame,5) end end
        end
    end
    if la and (tonumber(la.seq) or 0)>(tonumber(f._lastAnimatedActionSeq) or 0) then
        f._lastAnimatedActionSeq=tonumber(la.seq) or 0;local source;if la.kind=="heroattack" then source=(tonumber(la.side)==me and f._weaponAttackToken) or f._frameByKey["hero:"..tostring(la.side)] else source=f._frameByKey["unit:"..tostring(la.side)..":"..tostring(la.attacker)] or (f._deadTargets and f._deadTargets[tostring(la.side)..":"..tostring(la.attacker)]) end
        local targetSide=tonumber(la.side)==1 and 2 or 1;local target=(la.targetKind=="enemyHero") and f._frameByKey["hero:"..targetSide] or (f._frameByKey["unit:"..targetSide..":"..tostring(la.targetID)] or (f._deadTargets and f._deadTargets[tostring(targetSide)..":"..tostring(la.targetID)]));if source and target then PlayAttackLunge(source,target,function()PlayDamageBurst(target);PlayShake(target,la.targetKind=="enemyHero" and 7 or 5);PlayArenaImpactKick(f)end) end
    end
    f._snapshot=current
end

function RPAL.OpenHearthstoneMatch(match,viewerSide) RenderMatch(match,viewerSide) end
function RPAL.RefreshHearthstoneMatchUI(match,viewerSide) if matchFrame and matchFrame:IsShown() then RenderMatch(match,viewerSide) end end

function RPAL.HearthstoneReceiveEmote(sender,key,textValue)
    if matchFrame and matchFrame:IsShown() and matchFrame._showSpeechBubble then matchFrame._incomingEmoteText=tostring(textValue or "");matchFrame._showSpeechBubble(sender,key);matchFrame._incomingEmoteText=nil end
end


function RPAL.OnHearthstoneMatchPaused(match,sender)
    if matchFrame and matchFrame:IsShown() then matchFrame._allowHide=true;matchFrame:Hide();matchFrame._allowHide=nil end
    ShowStatus("Match paused. Re-challenge the same player later to resume it.")
    if frame and frame:IsShown() then RPAL.RefreshHearthstone() end
end
