-- Transmoggin - Copyright (c) 2026 Rooster_Roost. All Rights Reserved.
-- Proprietary software. Personal non-commercial use only; redistribution/repackaging/forks/reuse require permission.
-- Full license: Core.lua and LICENSE.txt

local T=Transmoggin
local C=T.COLORS

T.PHOTO_BACKGROUNDS={
    {key="studio", name="Dark Studio", category="STUDIO", solid=true},
}

T.PHOTO_EFFECTS={
    {key="none", name="None", mode="none", color={1,1,1}},
    {key="fire", name="Fire", mode="rise", color={1.00,0.20,0.03}},
    {key="frost", name="Frost", mode="snow", color={0.28,0.72,1.00}},
    {key="arcane", name="Arcane", mode="orbit", color={0.70,0.28,1.00}},
    {key="holy", name="Holy", mode="halo", color={1.00,0.82,0.32}},
    {key="shadow", name="Shadow", mode="smoke", color={0.42,0.12,0.58}},
    {key="fel", name="Fel", mode="rise", color={0.30,1.00,0.10}},
    {key="nature", name="Nature", mode="orbit", color={0.16,0.88,0.32}},
    {key="storm", name="Storm", mode="storm", color={0.48,0.76,1.00}},
    {key="blood", name="Blood", mode="smoke", color={0.85,0.05,0.06}},
    {key="ash", name="Ash", mode="ash", color={0.72,0.68,0.62}},
    {key="petals", name="Petals", mode="petals", color={1.00,0.42,0.70}},
    {key="motes", name="Motes", mode="motes", color={0.88,0.76,1.00}},
    {key="stars", name="Arcane Stars", mode="stars", color={0.76,0.58,1.00}},
    {key="wisps", name="Wisps", mode="wisp", color={0.42,0.88,1.00}},
    {key="sparks", name="Sparks", mode="sparks", color={1.00,0.56,0.10}},
    {key="radiance", name="Radiance", mode="radiance", color={1.00,0.90,0.50}},
    {key="shards", name="Icy Shards", mode="shards", color={0.58,0.88,1.00}},
}

T.PHOTO_ATMOSPHERES={
    {key="none", name="None", mode="none"},
    {key="mist", name="Mist", mode="mist", color={0.82,0.86,0.92}},
    {key="snow", name="Snow", mode="snow", color={0.88,0.95,1.00}},
    {key="embers", name="Embers", mode="embers", color={1.00,0.32,0.04}},
    {key="rain", name="Rain", mode="rain", color={0.62,0.78,1.00}},
    {key="dust", name="Dust", mode="dust", color={0.92,0.72,0.40}},
    {key="fireflies", name="Fireflies", mode="fireflies", color={0.70,1.00,0.28}},
    {key="ashfall", name="Ashfall", mode="ashfall", color={0.66,0.62,0.58}},
    {key="petalfall", name="Petal Fall", mode="petalfall", color={1.00,0.50,0.72}},
    {key="magicdust", name="Magic Dust", mode="magicdust", color={0.76,0.62,1.00}},
    {key="smokehaze", name="Smoke Haze", mode="smokehaze", color={0.42,0.40,0.46}},
}

T.PHOTO_DENSITIES={
    {key="low", name="Low", scale=.80},
    {key="normal", name="Normal", scale=1.20},
    {key="high", name="High", scale=1.75},
    {key="cinematic", name="Cinematic", scale=2.50},
}


local PHOTO_BACKGROUND_BY_KEY={}
for _,v in ipairs(T.PHOTO_BACKGROUNDS) do PHOTO_BACKGROUND_BY_KEY[v.key]=v end
local PHOTO_EFFECT_BY_KEY={}
for _,v in ipairs(T.PHOTO_EFFECTS) do PHOTO_EFFECT_BY_KEY[v.key]=v end
local PHOTO_ATMOSPHERE_BY_KEY={}
for _,v in ipairs(T.PHOTO_ATMOSPHERES) do PHOTO_ATMOSPHERE_BY_KEY[v.key]=v end
local PHOTO_DENSITY_BY_KEY={}
for _,v in ipairs(T.PHOTO_DENSITIES) do PHOTO_DENSITY_BY_KEY[v.key]=v end

local function rgba(t) return t[1],t[2],t[3],t[4] end
local function frame(tp,name,parent,template)
    if BackdropTemplateMixin and not template and tp~="DressUpModel" then template="BackdropTemplate" end
    return CreateFrame(tp,name,parent,template)
end
local function skin(f,bg,border,edge)
    if not f.SetBackdrop then return end
    f:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8",edgeFile="Interface\\Buttons\\WHITE8X8",edgeSize=edge or 1})
    f:SetBackdropColor(rgba(bg)); f:SetBackdropBorderColor(rgba(border))
end
local function fs(p,s,c,font)
    local x=p:CreateFontString(nil,"OVERLAY",font or "GameFontNormal")
    x:SetFont(STANDARD_TEXT_FONT,s,""); x:SetTextColor(rgba(c)); return x
end
local function button(p,label,w,h)
    local b=frame("Button",nil,p); b:SetSize(w,h); skin(b,{0.035,0.028,0.022,.94},C.gold2)
    b.txt=fs(b,11,C.gold); b.txt:SetPoint("CENTER"); b.txt:SetText(label)
    b:SetScript("OnEnter",function(self) if self.SetBackdropBorderColor then self:SetBackdropBorderColor(rgba(C.gold)) end end)
    b:SetScript("OnLeave",function(self) if self.SetBackdropBorderColor then self:SetBackdropBorderColor(rgba(C.gold2)) end end)
    return b
end

T.PHOTO_ANIMATIONS={
    {name="Idle",id=0,group="POSE"},
    {name="Ready",id=25,group="POSE"},
    {name="Kneel",id=75,group="POSE"},
    {name="Sit",id=97,group="POSE"},
    {name="Crouch",id=120,group="POSE"},
    {name="Salute",id=113,group="EMOTE"},
    {name="Wave",id=67,group="EMOTE"},
    {name="Cheer",id=68,group="EMOTE"},
    {name="Dance",id=69,group="EMOTE"},
    {name="Laugh",id=70,group="EMOTE"},
    {name="Roar",id=74,group="EMOTE"},
    {name="Flex",id=82,group="EMOTE"},
    {name="Point",id=84,group="EMOTE"},
    {name="Bow",id=66,group="EMOTE"},
    {name="Shrug",id=65,group="EMOTE"},
    {name="Applaud",id=80,group="EMOTE"},
    {name="Spin",id=126,group="ACTION"},
    {name="Run",id=5,group="ACTION"},
}

local function tryOnPiece(model,piece)
    if not model or not piece or not model.TryOn then return end
    local sourceID=piece.previewSourceID or piece.sourceID or piece.itemModID
    if sourceID and sourceID~=0 and pcall(model.TryOn,model,sourceID) then return end
    if piece.itemLink and pcall(model.TryOn,model,piece.itemLink) then return end
    if piece.itemID and piece.itemID~=0 then pcall(model.TryOn,model,piece.itemID) end
end

function T:RefreshPhotoModel()
    local m=self.photoModel; if not m then return end

    -- Photo Mode starts from the player's REAL equipped appearance. Do not Undress()
    -- here: doing so removed equipped weapons/armor and could leave temporary ghost
    -- silhouettes while source models were streaming in on first open.
    self.state.photoRefreshToken=(self.state.photoRefreshToken or 0)+1
    local token=self.state.photoRefreshToken
    if m.ClearModel then pcall(m.ClearModel,m) end
    pcall(m.SetUnit,m,"player")

    local function finishDress()
        if token~=T.state.photoRefreshToken or not T.photoModel or m~=T.photoModel then return end

        -- A Transmoggin working look acts as overrides on top of equipped gear.
        -- Empty/unselected slots therefore keep exactly what the character is wearing.
        for _,slot in ipairs(T.SLOTS or {}) do
            local piece=T.state.working and T.state.working.pieces and T.state.working.pieces[slot.key]
            if piece then tryOnPiece(m,piece) end
        end

        if m.SetSheathed then pcall(m.SetSheathed,m,T.state.photoSheathed~=false,false) end
        local anim=T.state.photoAnimation or 0
        if m.SetAnimation then pcall(m.SetAnimation,m,anim) end
    end

    -- Give DressUpModel one frame to finish SetUnit before applying transmog source
    -- overrides. This prevents the one-time floating/silhouette gear artifacts.
    if C_Timer and C_Timer.After then C_Timer.After(0.06,finishDress) else finishDress() end
end

function T:SetPhotoAnimation(animID,name)
    local m=self.photoModel; if not m then return end
    if m.HasAnimation then
        local ok,has=pcall(m.HasAnimation,m,animID)
        if ok and not has then self:Print((name or "That pose").." is not supported by this character model."); return end
    end
    self.state.photoAnimation=animID
    if m.SetAnimation then pcall(m.SetAnimation,m,animID) end
    if self.photoPoseName then self.photoPoseName:SetText(name or ("Animation "..tostring(animID))) end
end

function T:SetPhotoControlsVisible(show)
    if not self.photoMode then return end
    self.state.photoControlsVisible=show and true or false
    if self.photoMode.controls then self.photoMode.controls:Hide() end
    if self.photoMode.topBar then self.photoMode.topBar:SetShown(show) end
    if self.photoMode.dock then self.photoMode.dock:SetShown(show) end
    if not show and self.photoMode.backgroundPanel then self.photoMode.backgroundPanel:Hide() end
    if not show and self.photoMode.fxPanel then self.photoMode.fxPanel:Hide() end
end

function T:CapturePhoto()
    if not self.photoMode then return end
    local was=self.state.photoControlsVisible~=false
    self:SetPhotoControlsVisible(false)
    if self.photoMode.toggle then self.photoMode.toggle:Hide() end
    local function snap()
        if Screenshot then pcall(Screenshot) end
        local function restore()
            if T.photoMode and T.photoMode:IsShown() then
                T:SetPhotoControlsVisible(was)
                if T.photoMode.toggle then T.photoMode.toggle:Show() end
            end
        end
        if C_Timer and C_Timer.After then C_Timer.After(.45,restore) else restore() end
    end
    if C_Timer and C_Timer.After then C_Timer.After(.08,snap) else snap() end
end

function T:GetPhotoBackgroundDef(key)
    return PHOTO_BACKGROUND_BY_KEY[key] or self.PHOTO_BACKGROUNDS[1]
end



-- World Selfie Mode ---------------------------------------------------------
-- Uses the live game world as the background while temporarily hiding UI,
-- names, nameplates and chat bubbles. Other world characters remain visible.
local WORLD_SELFIE_CVARS={
    "nameplateShowAll",
    "nameplateShowEnemies",
    "nameplateShowFriends",
    "nameplateShowFriendlyNPCs",
    "nameplateShowEnemyMinions",
    "nameplateShowFriendlyMinions",
    "UnitNameOwn",
    "UnitNameNPC",
    "UnitNameFriendlyPlayerName",
    "UnitNameEnemyPlayerName",
    "UnitNameInteractiveNPC",
    "UnitNameNonInteractiveNPC",
    "UnitNameNonCombatCreatureName",
    "UnitNameFriendlyPetName",
    "UnitNameEnemyPetName",
    "UnitNamePlayerGuild",
    "UnitNamePlayerPVPTitle",
    "chatBubbles",
    "chatBubblesParty",
}

local function readWorldSelfieCVars()
    local saved={}
    for _,cvar in ipairs(WORLD_SELFIE_CVARS) do
        local ok,value=pcall(GetCVar,cvar)
        if ok and value~=nil then saved[cvar]=tostring(value) end
    end
    return saved
end

local function applyWorldSelfieCVars(values)
    if not values then return end
    for cvar,value in pairs(values) do pcall(SetCVar,cvar,tostring(value)) end
end

function T:SetWorldSelfieCleanCVars(enable)
    if enable then
        if self.worldSelfieSavedCVars then return end
        local saved=readWorldSelfieCVars()
        self.worldSelfieSavedCVars=saved
        if self.db and self.db.profile then self.db.profile.worldSelfieSavedCVars=saved end
        for _,cvar in ipairs(WORLD_SELFIE_CVARS) do pcall(SetCVar,cvar,"0") end
        return
    end

    local saved=self.worldSelfieSavedCVars
    if not saved and self.db and self.db.profile then saved=self.db.profile.worldSelfieSavedCVars end
    if not saved then return end

    applyWorldSelfieCVars(saved)
    self.worldSelfieSavedCVars=nil
    if self.db and self.db.profile then self.db.profile.worldSelfieSavedCVars=nil end

    -- Older clients may refresh world-name visibility on the next frame.
    if C_Timer and C_Timer.After then
        C_Timer.After(0,function() applyWorldSelfieCVars(saved) end)
    end
end

function T:RestoreStaleWorldSelfieCVars()
    local profile=self.db and self.db.profile
    local saved=profile and profile.worldSelfieSavedCVars
    if not saved then return end
    applyWorldSelfieCVars(saved)
    profile.worldSelfieSavedCVars=nil
    self.worldSelfieSavedCVars=nil
    if C_Timer and C_Timer.After then
        C_Timer.After(0,function() applyWorldSelfieCVars(saved) end)
    end
end

function T:EnsureWorldSelfieTooltip()
    if self.worldSelfieTooltip then return self.worldSelfieTooltip end
    local parent=WorldFrame or UIParent
    local tt=frame("Frame","TransmogginWorldSelfieTooltip",parent)
    self.worldSelfieTooltip=tt
    tt:SetFrameStrata("TOOLTIP"); tt:SetFrameLevel(10020); tt:SetSize(245,52)
    skin(tt,{0.018,0.012,0.010,.94},C.gold2,1)
    tt.title=fs(tt,12,C.gold); tt.title:SetPoint("TOPLEFT",10,-8); tt.title:SetPoint("TOPRIGHT",-10,-8); tt.title:SetJustifyH("LEFT")
    tt.body=fs(tt,10,{0.86,0.82,0.75,1}); tt.body:SetPoint("TOPLEFT",10,-25); tt.body:SetPoint("TOPRIGHT",-10,-25); tt.body:SetJustifyH("LEFT")
    tt:Hide()
    return tt
end

function T:ShowWorldSelfieTooltip(owner,title,body)
    local tt=self:EnsureWorldSelfieTooltip(); if not tt then return end
    tt.title:SetText(title or "")
    tt.body:SetText(body or "")
    tt:ClearAllPoints(); tt:SetPoint("BOTTOMRIGHT",owner,"TOPRIGHT",0,8); tt:Show()
end

function T:HideWorldSelfieTooltip()
    if self.worldSelfieTooltip then self.worldSelfieTooltip:Hide() end
end

function T:EnsureWorldSelfieFX()
    if self.worldSelfieFX then return self.worldSelfieFX end
    local parent=WorldFrame or UIParent
    local p=frame("Frame","TransmogginWorldSelfieFX",parent)
    self.worldSelfieFX=p
    p:SetAllPoints(parent); p:SetFrameStrata("TOOLTIP"); p:SetFrameLevel(9980); p:EnableMouse(false)
    local atmoLayer=frame("Frame",nil,p); p.atmoLayer=atmoLayer; atmoLayer:SetAllPoints(p); atmoLayer:SetFrameLevel(p:GetFrameLevel()+1); atmoLayer:EnableMouse(false)
    p.atmoSprites={}
    for i=1,96 do local tex=atmoLayer:CreateTexture(nil,"ARTWORK"); tex:Hide(); p.atmoSprites[i]=tex end
    local fxLayer=frame("Frame",nil,p); p.fxLayer=fxLayer; fxLayer:SetAllPoints(p); fxLayer:SetFrameLevel(p:GetFrameLevel()+2); fxLayer:EnableMouse(false)
    p.fxSprites={}
    for i=1,48 do local tex=fxLayer:CreateTexture(nil,"ARTWORK"); tex:Hide(); p.fxSprites[i]=tex end
    p:SetScript("OnUpdate",function(_,elapsed) if T.state and T.state.worldSelfie then T:UpdatePhotoStudioFX(elapsed) end end)
    p:Hide()
    return p
end

function T:EnsureWorldSelfieControls()
    if self.worldSelfieControls then return self.worldSelfieControls end
    local parent=WorldFrame or UIParent
    local f=frame("Frame","TransmogginWorldSelfieControls",parent)
    self.worldSelfieControls=f
    f:SetFrameStrata("TOOLTIP"); f:SetFrameLevel(10000); f:SetSize(198,44); f:SetPoint("BOTTOMRIGHT",parent,"BOTTOMRIGHT",-18,18)
    skin(f,{0.018,0.012,0.010,.80},C.gold2,1)

    local function selfieIconButton(tex,title,body)
        local b=frame("Button",nil,f); b:SetSize(42,38); skin(b,{0.018,0.012,0.010,.90},C.gold2,1)
        local i=b:CreateTexture(nil,"ARTWORK"); i:SetPoint("TOPLEFT",2,-2); i:SetPoint("BOTTOMRIGHT",-2,2); i:SetTexture(T.MEDIA..tex); i:SetTexCoord(.08,.92,.08,.92)
        b:SetScript("OnEnter",function(self) T:ShowWorldSelfieTooltip(self,title,body) end)
        b:SetScript("OnLeave",function() T:HideWorldSelfieTooltip() end)
        return b
    end
    local function selfiePanel(w,h)
        local p=frame("Frame",nil,f); p:SetFrameStrata("TOOLTIP"); p:SetFrameLevel(10010); p:SetSize(w,h); p:SetPoint("BOTTOMRIGHT",f,"TOPRIGHT",0,8)
        skin(p,{0.018,0.012,0.010,.94},C.gold2,1); p:Hide(); return p
    end
    local function closeSelfiePanels()
        if f.posePanel then f.posePanel:Hide() end
        if f.fxPanel then f.fxPanel:Hide() end
    end
    local function toggleSelfiePanel(panel)
        local was=panel:IsShown(); closeSelfiePanels(); if not was then panel:Show() end
    end

    local cap=selfieIconButton("photo_take.tga","Take Photo","Capture a clean screenshot with your selected Photo Studio effects.")
    cap:SetPoint("LEFT",4,0); cap:SetScript("OnClick",function() closeSelfiePanels(); T:CaptureWorldSelfie() end)

    local pose=selfieIconButton("photo_pose.tga","Poses","Use player emotes and poses while photographing the live world.")
    pose:SetPoint("LEFT",cap,"RIGHT",6,0)

    local fx=selfieIconButton("photo_fx.tga","FX","Change Photo Studio effects and atmosphere without leaving World Selfie.")
    fx:SetPoint("LEFT",pose,"RIGHT",6,0)

    local back=selfieIconButton("photo_exit.tga","Return to Studio","Leave World Selfie and return to Transmoggin Photo Studio.")
    back:SetPoint("LEFT",fx,"RIGHT",6,0); back:SetScript("OnClick",function() closeSelfiePanels(); T:ExitWorldSelfieMode() end)

    -- World Selfie uses the real player model, so poses are driven by normal
    -- player emotes rather than DressUpModel animation IDs.
    local posePanel=selfiePanel(250,222); f.posePanel=posePanel
    local pt=fs(posePanel,14,C.gold); pt:SetPoint("TOPLEFT",12,-10); pt:SetText("WORLD POSES")
    local worldPoses={
        {"Stand","STAND"},{"Ready","READY"},{"Kneel","KNEEL"},{"Sit","SIT"},
        {"Salute","SALUTE"},{"Wave","WAVE"},{"Cheer","CHEER"},{"Dance","DANCE"},
        {"Roar","ROAR"},{"Flex","FLEX"},{"Point","POINT"},{"Bow","BOW"},
    }
    for i,v in ipairs(worldPoses) do
        local col=(i-1)%2; local row=math.floor((i-1)/2)
        local b=button(posePanel,v[1],108,24); b:SetPoint("TOPLEFT",12+col*116,-36-row*28)
        b:SetScript("OnClick",function()
            if DoEmote then pcall(DoEmote,v[2]) end
        end)
    end
    pose:SetScript("OnClick",function() toggleSelfiePanel(posePanel) end)

    -- World Selfie FX uses a small toolbar popover. It modifies the same Photo
    -- Studio state, but stays compact enough to keep the live scene visible.
    local fxPanel=selfiePanel(214,138); f.fxPanel=fxPanel
    fxPanel:ClearAllPoints(); fxPanel:SetPoint("BOTTOMRIGHT",fx,"TOPRIGHT",0,7)
    local ft=fs(fxPanel,11,C.gold); ft:SetPoint("TOPLEFT",10,-8); ft:SetText("FX")

    local function cycleDef(list,key,dir,setter)
        local idx=1
        for i,v in ipairs(list) do if v.key==key then idx=i break end end
        idx=((idx-1+(dir or 1))%#list)+1
        setter(T,list[idx].key,true)
        if fxPanel.refresh then fxPanel.refresh() end
    end
    local function addCycleRow(y,label)
        local lbl=fs(fxPanel,9,{0.72,0.67,0.58,1}); lbl:SetPoint("TOPLEFT",10,y); lbl:SetText(label)
        local prev=button(fxPanel,"<",24,20); prev:SetPoint("TOPLEFT",58,y+4)
        local val=button(fxPanel,"",96,20); val:SetPoint("LEFT",prev,"RIGHT",4,0)
        local nextb=button(fxPanel,">",24,20); nextb:SetPoint("LEFT",val,"RIGHT",4,0)
        return prev,val,nextb
    end

    local ePrev,eVal,eNext=addCycleRow(-27,"Effect")
    local aPrev,aVal,aNext=addCycleRow(-53,"Atmos")
    local iPrev,iVal,iNext=addCycleRow(-79,"Power")
    local dPrev,dVal,dNext=addCycleRow(-105,"Density")

    local function refreshWorldFXText()
        local e=T:GetPhotoEffectDef(T.state.photoEffect)
        local a=T:GetPhotoAtmosphereDef(T.state.photoAtmosphere)
        local d=T.GetPhotoDensityDef and T:GetPhotoDensityDef(T.state.photoDensity) or nil
        eVal.txt:SetText(e and e.name or "None")
        aVal.txt:SetText(a and a.name or "None")
        local intensity=math.floor(((T.state.photoEffectIntensity or 1)*100)+0.5)
        iVal.txt:SetText(intensity.."%")
        dVal.txt:SetText((d and d.name) or tostring(T.state.photoDensity or "Normal"))
    end
    fxPanel.refresh=refreshWorldFXText

    ePrev:SetScript("OnClick",function() cycleDef(T.PHOTO_EFFECTS,T.state.photoEffect,-1,T.SetPhotoEffect) end)
    eNext:SetScript("OnClick",function() cycleDef(T.PHOTO_EFFECTS,T.state.photoEffect,1,T.SetPhotoEffect) end)
    eVal:SetScript("OnClick",function() cycleDef(T.PHOTO_EFFECTS,T.state.photoEffect,1,T.SetPhotoEffect) end)
    aPrev:SetScript("OnClick",function() cycleDef(T.PHOTO_ATMOSPHERES,T.state.photoAtmosphere,-1,T.SetPhotoAtmosphere) end)
    aNext:SetScript("OnClick",function() cycleDef(T.PHOTO_ATMOSPHERES,T.state.photoAtmosphere,1,T.SetPhotoAtmosphere) end)
    aVal:SetScript("OnClick",function() cycleDef(T.PHOTO_ATMOSPHERES,T.state.photoAtmosphere,1,T.SetPhotoAtmosphere) end)
    iPrev:SetScript("OnClick",function() T:AdjustPhotoEffectIntensity(-.10); refreshWorldFXText() end)
    iNext:SetScript("OnClick",function() T:AdjustPhotoEffectIntensity(.10); refreshWorldFXText() end)
    iVal:SetScript("OnClick",function() T:AdjustPhotoEffectIntensity(.10); refreshWorldFXText() end)
    dPrev:SetScript("OnClick",function() cycleDef(T.PHOTO_DENSITIES,T.state.photoDensity,-1,T.SetPhotoDensity) end)
    dNext:SetScript("OnClick",function() cycleDef(T.PHOTO_DENSITIES,T.state.photoDensity,1,T.SetPhotoDensity) end)
    dVal:SetScript("OnClick",function() cycleDef(T.PHOTO_DENSITIES,T.state.photoDensity,1,T.SetPhotoDensity) end)
    fxPanel:SetScript("OnShow",refreshWorldFXText)
    fx:SetScript("OnClick",function() toggleSelfiePanel(fxPanel) end)

    f.closePanels=closeSelfiePanels
    f:Hide()
    return f
end

function T:CaptureWorldSelfie()
    if not self.state or not self.state.worldSelfie then return end
    local f=self.worldSelfieControls
    if f then f:Hide() end
    self:HideWorldSelfieTooltip()
    local function snap()
        if Screenshot then pcall(Screenshot) end
        local function restore()
            if T.state and T.state.worldSelfie and T.worldSelfieControls then T.worldSelfieControls:Show() end
        end
        if C_Timer and C_Timer.After then C_Timer.After(.45,restore) else restore() end
    end
    if C_Timer and C_Timer.After then C_Timer.After(.08,snap) else snap() end
end


local function SaveAndHideWorldSelfieHUD(self)
    self.worldSelfieHudState = self.worldSelfieHudState or {}

    local state = self.worldSelfieHudState
    state.uiParentShown = UIParent and UIParent:IsShown() or false

    -- Keep our World Selfie overlay alive independently of UIParent.
    if self.worldSelfieControls then
        self.worldSelfieControls:SetParent(WorldFrame)
        self.worldSelfieControls:SetFrameStrata("TOOLTIP")
    end
    if self.worldSelfieTooltip then
        self.worldSelfieTooltip:SetParent(WorldFrame)
        self.worldSelfieTooltip:SetFrameStrata("TOOLTIP")
    end
    if self.worldSelfieFX then
        self.worldSelfieFX:SetParent(WorldFrame)
        self.worldSelfieFX:SetFrameStrata("FULLSCREEN_DIALOG")
    end

    if UIParent then
        UIParent:Hide()
    end
end

local function RestoreWorldSelfieHUD(self)
    local state = self.worldSelfieHudState
    if not state then return end

    if UIParent and state.uiParentShown then
        UIParent:Show()
    end

    -- Return addon-owned frames to UIParent after leaving World Selfie.
    if self.worldSelfieControls then
        self.worldSelfieControls:SetParent(UIParent)
    end
    if self.worldSelfieTooltip then
        self.worldSelfieTooltip:SetParent(UIParent)
    end
    if self.worldSelfieFX then
        self.worldSelfieFX:SetParent(UIParent)
    end

    self.worldSelfieHudState = nil
end

function T:EnterWorldSelfieMode()
    if not self.state or not self.state.photoMode or self.state.worldSelfie then return end
    self.state.worldSelfie=true
    if self.photoMode then
        if self.photoMode.backgroundPanel then self.photoMode.backgroundPanel:Hide() end
        if self.photoMode.fxPanel then self.photoMode.fxPanel:Hide() end
        if self.photoMode.posePanel then self.photoMode.posePanel:Hide() end
        if self.photoMode.cameraPanel then self.photoMode.cameraPanel:Hide() end
        if self.photoMode.presetPanel then self.photoMode.presetPanel:Hide() end
        self.photoMode:Hide()
    end
    self:SetWorldSelfieCleanCVars(true)
    local fx=self:EnsureWorldSelfieFX()
    local f=self:EnsureWorldSelfieControls()
    SaveAndHideWorldSelfieHUD(self)
    if fx then fx:Show() end
    if f then f:Show() end
end

function T:ExitWorldSelfieMode()
    if not self.state or not self.state.worldSelfie then return end
    self.state.worldSelfie=false
    if self.worldSelfieControls then if self.worldSelfieControls.closePanels then self.worldSelfieControls.closePanels() end; self.worldSelfieControls:Hide() end
    if self.worldSelfieTooltip then self.worldSelfieTooltip:Hide() end
    self:StopAllPhotoFX()
    self:SetWorldSelfieCleanCVars(false)
    RestoreWorldSelfieHUD(self)
    if self.state.photoMode and self.photoMode then
        self.photoMode:Show()
        self:SetPhotoControlsVisible(true)
        self:RefreshPhotoModel()
    end
end

function T:GetPhotoEffectDef(key) return PHOTO_EFFECT_BY_KEY[key or "none"] or self.PHOTO_EFFECTS[1] end
function T:GetPhotoAtmosphereDef(key) return PHOTO_ATMOSPHERE_BY_KEY[key or "none"] or self.PHOTO_ATMOSPHERES[1] end
function T:GetPhotoDensityDef(key) return PHOTO_DENSITY_BY_KEY[key or "high"] or self.PHOTO_DENSITIES[3] end

function T:ApplyPhotoModelPosition(save)
    local p=self.photoMode; local m=self.photoModel
    if not p or not m then return end
    local x=self.state.photoPosX or 0
    local y=self.state.photoPosY or 0
    local w=UIParent:GetWidth(); local h=UIParent:GetHeight()
    m:ClearAllPoints()
    m:SetPoint("TOPLEFT",UIParent,"TOPLEFT",w*(.14+x),-80+(h*y))
    m:SetPoint("BOTTOMRIGHT",UIParent,"BOTTOMRIGHT",-w*(.22-x),-20+(h*y))
    if save and self.db and self.db.profile then
        self.db.profile.photo=self.db.profile.photo or {}
        self.db.profile.photo.posX=x; self.db.profile.photo.posY=y
    end
end

local function photoTex(tex,path,r,g,b,a,blend)
    if not tex then return end
    local fullPath=T.MEDIA..path
    if tex.__tmTexture~=fullPath then
        tex:SetTexture(fullPath)
        tex.__tmTexture=fullPath
    end
    if tex.SetVertexColor then tex:SetVertexColor(r or 1,g or 1,b or 1) end
    tex:SetAlpha(a or 1)
    if tex.SetBlendMode and blend and tex.__tmBlend~=blend then
        pcall(tex.SetBlendMode,tex,blend)
        tex.__tmBlend=blend
    end
    tex:Show()
end

local function hidePhotoSprites(list)
    for _,tex in ipairs(list or {}) do tex:Hide() end
end

function T:ApplyPhotoVisuals(save)
    local p=self.photoMode; if not p then return end
    local fx=self:GetPhotoEffectDef(self.state.photoEffect)
    local atmo=self:GetPhotoAtmosphereDef(self.state.photoAtmosphere)
    local intensity=math.max(0,math.min(1,self.state.photoEffectIntensity or .78))
    local dim=math.max(0,math.min(.55,self.state.photoDim or 0))
    local density=self:GetPhotoDensityDef(self.state.photoDensity) or self.PHOTO_DENSITIES[3]

    if p.backgroundDim then
        p.backgroundDim:SetVertexColor(0,0,0)
        p.backgroundDim:SetAlpha(dim)
        if dim>.001 then p.backgroundDim:Show() else p.backgroundDim:Hide() end
    end
    if p.sceneTint then p.sceneTint:Hide() end
    if p.effectTint then p.effectTint:Hide() end
    if p.atmosphereTint then p.atmosphereTint:Hide() end
    if p.vignette then p.vignette:Hide() end

    if p.fxStatus then
        p.fxStatus:SetText(string.format("FX: %s   Atmosphere: %s   Strength: %d%%   Density: %s", fx and fx.name or "None", atmo and atmo.name or "None", math.floor(intensity*100+.5), density and density.name or "High"))
    end
    if save and self.db and self.db.profile then
        local ph=self.db.profile.photo or {}; self.db.profile.photo=ph
        ph.effect=self.state.photoEffect; ph.atmosphere=self.state.photoAtmosphere
        ph.effectIntensity=intensity; ph.dim=dim; ph.density=self.state.photoDensity or "high"
    end
end

function T:StopAllPhotoFX()
    local function clear(frame)
        if not frame then return end
        hidePhotoSprites(frame.fxSprites)
        hidePhotoSprites(frame.atmoSprites)
        if frame.effectTint then frame.effectTint:Hide() end
        if frame.atmosphereTint then frame.atmosphereTint:Hide() end
        if frame.sceneTint then frame.sceneTint:Hide() end
        if frame.vignette then frame.vignette:Hide() end
    end
    clear(self.photoMode)
    clear(self.worldSelfieFX)
    if self.worldSelfieFX then self.worldSelfieFX:Hide() end
end

function T:UpdatePhotoStudioFX(elapsed)
    -- FX are valid ONLY while Transmoggin Photo Mode is active.
    -- World Selfie uses its WorldFrame overlay; normal studio uses the Photo Mode frame.
    if not self.state or not self.state.photoMode then
        self:StopAllPhotoFX()
        return
    end

    local p
    if self.state.worldSelfie then
        p=self.worldSelfieFX
        if not p or not p:IsShown() then return end
    else
        p=self.photoMode
        if not p or not p:IsShown() then
            self:StopAllPhotoFX()
            return
        end
        -- Never leave the World Selfie overlay alive while back in Studio.
        if self.worldSelfieFX and self.worldSelfieFX:IsShown() then
            hidePhotoSprites(self.worldSelfieFX.fxSprites)
            hidePhotoSprites(self.worldSelfieFX.atmoSprites)
            self.worldSelfieFX:Hide()
        end
    end
    p.__fxElapsed=(p.__fxElapsed or 0)+elapsed
    if p.__fxElapsed<.025 then return end
    p.__fxElapsed=0

    local now=GetTime and GetTime() or 0
    local intensity=math.max(0,math.min(1,self.state.photoEffectIntensity or .78))
    local fx=self:GetPhotoEffectDef(self.state.photoEffect)
    local atmo=self:GetPhotoAtmosphereDef(self.state.photoAtmosphere)
    local sprites=p.fxSprites or {}
    local atmos=p.atmoSprites or {}
    local density=self:GetPhotoDensityDef(self.state.photoDensity) or self.PHOTO_DENSITIES[3]
    local densityScale=(density and density.scale) or 1
    local function scaled(base,cap) return math.max(1,math.min(cap,math.floor(base*densityScale+.5))) end
    hidePhotoSprites(sprites); hidePhotoSprites(atmos)

    -- Strong cinematic intensity is intentional by default.
    -- FX are localized around the subject while atmosphere fills the scene.
    local power=.55+(intensity*.75)

    if fx and fx.mode~="none" and intensity>.01 then
        local r,g,b=fx.color[1],fx.color[2],fx.color[3]
        if fx.mode=="rise" then
            -- Fire/Fel: dense rising field from feet to above the shoulders.
            for i=1,scaled(22,#sprites-3) do
                local tex=sprites[i]; if tex then
                    local phase=(i*.083)%1; local speed=.24+(i%5)*.018
                    local progress=(now*speed+phase)%1
                    local lane=((i*67)%360)-180
                    local x=lane+math.sin(now*(1.15+(i%3)*.12)+i)*42
                    local y=-250+progress*525
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.fxLayer,"CENTER",x,y)
                    local sz=18+(i%5)*7
                    tex:SetSize(sz,sz)
                    photoTex(tex,(i%4==0) and "fx_spark.tga" or "fx_mote.tga",r,g,b,(.34+.46*(1-progress))*power,"ADD")
                end
            end
            local glow=sprites[#sprites]; if glow then glow:ClearAllPoints(); glow:SetPoint("CENTER",p.fxLayer,"CENTER",0,-35); glow:SetSize(430,560); photoTex(glow,"fx_glow.tga",r,g,b,.19*power,"ADD") end
            local glow2=sprites[#sprites-1]; if glow2 then glow2:ClearAllPoints(); glow2:SetPoint("CENTER",p.fxLayer,"CENTER",0,115); glow2:SetSize(315,350); photoTex(glow2,"fx_glow.tga",r,g,b,.11*power,"ADD") end
        elseif fx.mode=="snow" then
            -- Frost: broad icy field plus a cool body glow.
            for i=1,scaled(24,#sprites-2) do
                local tex=sprites[i]; if tex then
                    local phase=(i*.061)%1; local progress=(now*(.14+(i%6)*.008)+phase)%1
                    local x=-220+((i*97)%440)+math.sin(now*.8+i)*42
                    local y=285-progress*585
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.fxLayer,"CENTER",x,y)
                    local sz=12+(i%5)*5; tex:SetSize(sz,sz)
                    photoTex(tex,(i%3==0) and "fx_mote.tga" or "fx_spark.tga",r,g,b,(.42+.32*(i%2))*power,"ADD")
                end
            end
            local glow=sprites[#sprites]; if glow then glow:ClearAllPoints(); glow:SetPoint("CENTER",p.fxLayer,"CENTER",0,-5); glow:SetSize(390,500); photoTex(glow,"fx_glow.tga",r,g,b,.14*power,"ADD") end
        elseif fx.mode=="orbit" then
            -- Arcane/Nature: multiple orbital rings at different speeds/radii.
            for i=1,scaled(22,#sprites-3) do
                local tex=sprites[i]; if tex then
                    local ring=(i%3)
                    local speed=.72+ring*.27
                    local ang=now*speed+(i/22)*12.566
                    local radius=95+ring*48+18*math.sin(now*.8+i)
                    local x=math.cos(ang)*radius
                    local y=math.sin(ang)*(145+ring*42)
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.fxLayer,"CENTER",x,y)
                    local sz=18+(i%4)*7; tex:SetSize(sz,sz)
                    photoTex(tex,(i%4==0) and "fx_mote.tga" or "fx_spark.tga",r,g,b,.62*power,"ADD")
                end
            end
            local glow=sprites[#sprites]; if glow then glow:ClearAllPoints(); glow:SetPoint("CENTER",p.fxLayer,"CENTER",0,-5); glow:SetSize(390,500); photoTex(glow,"fx_glow.tga",r,g,b,.14*power,"ADD") end
        elseif fx.mode=="halo" then
            -- Holy: strong radiance with layered beams around the upper body.
            local glow=sprites[1]; if glow then glow:ClearAllPoints(); glow:SetPoint("CENTER",p.fxLayer,"CENTER",0,75); glow:SetSize(500,500); photoTex(glow,"fx_glow.tga",r,g,b,.31*power,"ADD") end
            local glow2=sprites[2]; if glow2 then glow2:ClearAllPoints(); glow2:SetPoint("CENTER",p.fxLayer,"CENTER",0,-35); glow2:SetSize(350,470); photoTex(glow2,"fx_glow.tga",r,g,b,.14*power,"ADD") end
            for i=3,2+scaled(20,#sprites-4) do
                local tex=sprites[i]; if tex then
                    local ang=(i/20)*6.283+now*.28
                    local x=math.cos(ang)*(120+(i%4)*24)
                    local y=85+math.sin(ang)*(165+(i%3)*25)
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.fxLayer,"CENTER",x,y)
                    tex:SetSize(14+(i%3)*5,80+(i%4)*24)
                    photoTex(tex,(i%3==0) and "fx_spark.tga" or "fx_streak.tga",r,g,b,.30*power,"ADD")
                end
            end
        elseif fx.mode=="smoke" then
            -- Shadow/Blood: dense rolling smoke around the full silhouette.
            for i=1,scaled(18,#sprites-2) do
                local tex=sprites[i]; if tex then
                    local phase=(i*.089)%1; local progress=(now*(.075+(i%4)*.008)+phase)%1
                    local x=math.sin(now*(.42+(i%3)*.05)+i*1.9)*(105+(i%5)*18)
                    local y=-215+progress*455
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.fxLayer,"CENTER",x,y)
                    local sz=125+(i%5)*34; tex:SetSize(sz,sz)
                    photoTex(tex,"fx_smoke.tga",r,g,b,(.14+.18*(1-progress))*power,"BLEND")
                end
            end
            local glow=sprites[#sprites]; if glow then glow:ClearAllPoints(); glow:SetPoint("CENTER",p.fxLayer,"CENTER",0,-20); glow:SetSize(410,520); photoTex(glow,"fx_glow.tga",r,g,b,.13*power,"ADD") end
        elseif fx.mode=="ash" then
            local n=scaled(28,#sprites-2)
            for i=1,n do
                local tex=sprites[i]; if tex then
                    local phase=(i*.047)%1; local progress=(now*(.08+(i%5)*.006)+phase)%1
                    local x=-230+((i*91)%460)+math.sin(now*.45+i)*55
                    local y=300-progress*610
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.fxLayer,"CENTER",x,y)
                    local sz=8+(i%4)*5; tex:SetSize(sz,sz)
                    photoTex(tex,"fx_mote.tga",r,g,b,.42*power,"BLEND")
                end
            end
        elseif fx.mode=="petals" then
            local n=scaled(26,#sprites-2)
            for i=1,n do
                local tex=sprites[i]; if tex then
                    local phase=(i*.053)%1; local progress=(now*(.10+(i%4)*.008)+phase)%1
                    local x=-240+((i*103)%480)+math.sin(now*.7+i)*70
                    local y=315-progress*620
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.fxLayer,"CENTER",x,y)
                    tex:SetSize(12+(i%3)*6,18+(i%4)*7)
                    photoTex(tex,"fx_mote.tga",r,g,b,.58*power,"ADD")
                end
            end
        elseif fx.mode=="motes" or fx.mode=="stars" then
            local n=scaled(30,#sprites-2)
            for i=1,n do
                local tex=sprites[i]; if tex then
                    local ang=now*(.25+(i%4)*.04)+i*1.7
                    local x=math.sin(ang)*(75+(i%6)*33)
                    local y=-180+((i*83)%430)+math.cos(ang*.73)*45
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.fxLayer,"CENTER",x,y)
                    local sz=(fx.mode=="stars" and 10 or 14)+(i%4)*5; tex:SetSize(sz,sz)
                    photoTex(tex,(fx.mode=="stars" or i%3==0) and "fx_spark.tga" or "fx_mote.tga",r,g,b,.66*power,"ADD")
                end
            end
            local glow=sprites[#sprites]; if glow then glow:ClearAllPoints(); glow:SetPoint("CENTER",p.fxLayer,"CENTER",0,0); glow:SetSize(420,520); photoTex(glow,"fx_glow.tga",r,g,b,.11*power,"ADD") end
        elseif fx.mode=="wisp" then
            local n=scaled(20,#sprites-2)
            for i=1,n do
                local tex=sprites[i]; if tex then
                    local t=now*(.34+(i%3)*.04)+i
                    local x=math.sin(t*1.2)*(115+(i%4)*25)
                    local y=math.cos(t*.8)*(180+(i%5)*18)
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.fxLayer,"CENTER",x,y)
                    tex:SetSize(85+(i%4)*25,85+(i%4)*25)
                    photoTex(tex,"fx_smoke.tga",r,g,b,.16*power,"ADD")
                end
            end
        elseif fx.mode=="sparks" then
            local n=scaled(34,#sprites-2)
            for i=1,n do
                local tex=sprites[i]; if tex then
                    local phase=(i*.039)%1; local progress=(now*(.22+(i%6)*.012)+phase)%1
                    local x=-250+((i*117)%500)+math.sin(now*1.1+i)*35
                    local y=-250+progress*540
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.fxLayer,"CENTER",x,y)
                    local sz=10+(i%5)*5; tex:SetSize(sz,sz)
                    photoTex(tex,"fx_spark.tga",r,g,b,.72*power,"ADD")
                end
            end
        elseif fx.mode=="radiance" then
            local glow=sprites[#sprites]; if glow then glow:ClearAllPoints(); glow:SetPoint("CENTER",p.fxLayer,"CENTER",0,40); glow:SetSize(560,620); photoTex(glow,"fx_glow.tga",r,g,b,.30*power,"ADD") end
            local n=scaled(24,#sprites-2)
            for i=1,n do
                local tex=sprites[i]; if tex then
                    local ang=(i/n)*6.283+now*.18
                    local x=math.cos(ang)*(145+(i%4)*22)
                    local y=45+math.sin(ang)*(205+(i%3)*28)
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.fxLayer,"CENTER",x,y)
                    tex:SetSize(12+(i%3)*4,100+(i%5)*24)
                    photoTex(tex,"fx_streak.tga",r,g,b,.33*power,"ADD")
                end
            end
        elseif fx.mode=="shards" then
            local n=scaled(30,#sprites-2)
            for i=1,n do
                local tex=sprites[i]; if tex then
                    local phase=(i*.043)%1; local progress=(now*(.14+(i%5)*.009)+phase)%1
                    local x=-240+((i*127)%480)+math.sin(now*.62+i)*44
                    local y=300-progress*600
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.fxLayer,"CENTER",x,y)
                    tex:SetSize(8+(i%4)*4,30+(i%5)*10)
                    photoTex(tex,"fx_streak.tga",r,g,b,.48*power,"ADD")
                end
            end
        elseif fx.mode=="storm" then
            -- Storm: fast sparks on several rings, plus frequent lightning strikes.
            for i=1,scaled(20,#sprites-7) do
                local tex=sprites[i]; if tex then
                    local ring=i%3
                    local ang=now*(1.55+ring*.34)+(i/20)*12.566
                    local x=math.cos(ang)*(105+ring*55)
                    local y=math.sin(ang)*(155+ring*42)
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.fxLayer,"CENTER",x,y)
                    local sz=17+(i%4)*6; tex:SetSize(sz,sz)
                    photoTex(tex,"fx_spark.tga",r,g,b,.72*power,"ADD")
                end
            end
            local flashStep=math.floor(now*8)
            for i=#sprites-5,#sprites-2 do
                local tex=sprites[i]; if tex then
                    local flash=((flashStep+i*2)%7)==0 or ((flashStep+i)%13)==0
                    if flash then
                        local side=((i%2)==0) and 1 or -1
                        local x=side*(105+((i*31)%85)); local y=20+((i*47)%120)
                        tex:ClearAllPoints(); tex:SetPoint("CENTER",p.fxLayer,"CENTER",x,y); tex:SetSize(125,310)
                        if tex.SetTexCoord then if side<0 then tex:SetTexCoord(0,1,0,1) else tex:SetTexCoord(1,0,0,1) end end
                        photoTex(tex,"fx_lightning.tga",r,g,b,.82*power,"ADD")
                    end
                end
            end
            local glow=sprites[#sprites]; if glow then glow:ClearAllPoints(); glow:SetPoint("CENTER",p.fxLayer,"CENTER",0,-5); glow:SetSize(410,520); photoTex(glow,"fx_glow.tga",r,g,b,.15*power,"ADD") end
        end
    end

    if atmo and atmo.mode~="none" then
        local color=atmo.color or {1,1,1}; local r,g,b=color[1],color[2],color[3]
        local apower=.60+(intensity*.55)
        if atmo.mode=="mist" then
            for i=1,scaled(14,#atmos) do
                local tex=atmos[i]; if tex then
                    local x=((now*(24+i*1.8)+i*211)%2400)-1200
                    local y=-350+(i%5)*175+math.sin(now*.18+i)*45
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.atmoLayer,"CENTER",x,y); tex:SetSize(600+(i%3)*120,340+(i%2)*80)
                    photoTex(tex,"fx_smoke.tga",r,g,b,.085*apower,"BLEND")
                end
            end
        elseif atmo.mode=="snow" or atmo.mode=="rain" then
            for i=1,scaled(34,#atmos) do
                local tex=atmos[i]; if tex then
                    local phase=(i*.037)%1
                    local speed=atmo.mode=="rain" and (.58+(i%5)*.025) or (.17+(i%6)*.008)
                    local progress=(now*speed+phase)%1
                    local x=-980+((i*137)%1960)+math.sin(now*.8+i)*35
                    local y=600-progress*1220
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.atmoLayer,"CENTER",x,y)
                    if atmo.mode=="rain" then tex:SetSize(18,115+(i%4)*18); photoTex(tex,"fx_streak.tga",r,g,b,.27*apower,"ADD")
                    else local sz=7+(i%5)*5; tex:SetSize(sz,sz); photoTex(tex,(i%4==0) and "fx_spark.tga" or "fx_mote.tga",r,g,b,.62*apower,"ADD") end
                end
            end
        elseif atmo.mode=="embers" then
            for i=1,scaled(30,#atmos) do
                local tex=atmos[i]; if tex then
                    local phase=(i*.041)%1; local progress=(now*(.12+(i%7)*.006)+phase)%1
                    local x=-930+((i*131)%1860)+math.sin(now*.75+i)*65
                    local y=-610+progress*1220
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.atmoLayer,"CENTER",x,y)
                    local sz=8+(i%5)*5; tex:SetSize(sz,sz)
                    photoTex(tex,(i%5==0) and "fx_spark.tga" or "fx_mote.tga",r,g,b,.58*apower,"ADD")
                end
            end
        elseif atmo.mode=="ashfall" or atmo.mode=="petalfall" or atmo.mode=="magicdust" then
            local n=scaled(38,#atmos)
            for i=1,n do
                local tex=atmos[i]; if tex then
                    local phase=(i*.031)%1; local speed=(atmo.mode=="ashfall") and .11 or .14
                    local progress=(now*(speed+(i%6)*.006)+phase)%1
                    local x=-980+((i*151)%1960)+math.sin(now*.55+i)*65
                    local y=610-progress*1240
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.atmoLayer,"CENTER",x,y)
                    local sz=(atmo.mode=="petalfall") and (12+(i%4)*5) or (7+(i%4)*4)
                    tex:SetSize(sz,atmo.mode=="petalfall" and sz*1.45 or sz)
                    photoTex(tex,(atmo.mode=="magicdust" or i%5==0) and "fx_spark.tga" or "fx_mote.tga",r,g,b,(atmo.mode=="ashfall" and .35 or .58)*apower,"ADD")
                end
            end
        elseif atmo.mode=="smokehaze" then
            local n=scaled(18,#atmos)
            for i=1,n do
                local tex=atmos[i]; if tex then
                    local x=((now*(18+i)+i*177)%2400)-1200
                    local y=-420+(i%6)*155+math.sin(now*.15+i)*55
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.atmoLayer,"CENTER",x,y)
                    tex:SetSize(650+(i%4)*120,360+(i%3)*70)
                    photoTex(tex,"fx_smoke.tga",r,g,b,.10*apower,"BLEND")
                end
            end
        elseif atmo.mode=="dust" or atmo.mode=="fireflies" then
            for i=1,scaled(30,#atmos) do
                local tex=atmos[i]; if tex then
                    local x=-900+((i*149)%1800)+math.sin(now*(.28+(i%5)*.01)+i)*75
                    local y=-500+((i*113)%1000)+math.cos(now*.31+i)*55
                    tex:ClearAllPoints(); tex:SetPoint("CENTER",p.atmoLayer,"CENTER",x,y)
                    local sz=atmo.mode=="fireflies" and (11+(i%4)*4) or (7+(i%3)*3); tex:SetSize(sz,sz)
                    photoTex(tex,"fx_mote.tga",r,g,b,(atmo.mode=="fireflies" and .62 or .31)*apower,"ADD")
                end
            end
        end
    end
end

function T:SetPhotoEffect(key,save)
    local def=self:GetPhotoEffectDef(key); if not def then return end
    self.state.photoEffect=def.key; self:ApplyPhotoVisuals(save)
end
function T:SetPhotoAtmosphere(key,save)
    local def=self:GetPhotoAtmosphereDef(key); if not def then return end
    self.state.photoAtmosphere=def.key; self:ApplyPhotoVisuals(save)
end
function T:SetPhotoDensity(key,save)
    local def=self:GetPhotoDensityDef(key); if not def then return end
    self.state.photoDensity=def.key
    self:ApplyPhotoVisuals(save)
end
function T:AdjustPhotoEffectIntensity(delta)
    self.state.photoEffectIntensity=math.max(0,math.min(1,(self.state.photoEffectIntensity or .78)+delta)); self:ApplyPhotoVisuals(true)
end
function T:AdjustPhotoDim(delta)
    self.state.photoDim=math.max(0,math.min(.65,(self.state.photoDim or 0)+delta)); self:ApplyPhotoVisuals(true)
end
function T:NudgePhotoModel(dx,dy)
    self.state.photoPosX=math.max(-.12,math.min(.12,(self.state.photoPosX or 0)+dx))
    self.state.photoPosY=math.max(-.12,math.min(.12,(self.state.photoPosY or 0)+dy))
    self:ApplyPhotoModelPosition(true)
end
function T:ResetPhotoComposition()
    self.state.photoZoom=.78; self.state.photoPosX=0; self.state.photoPosY=0
    self.state.photoEffect="none"; self.state.photoAtmosphere="none"; self.state.photoEffectIntensity=.78; self.state.photoDensity="high"; self.state.photoDim=0
    if self.photoModel and self.photoModel.SetCamDistanceScale then pcall(self.photoModel.SetCamDistanceScale,self.photoModel,.78) end
    if self.photoModel and self.photoModel.SetFacing then pcall(self.photoModel.SetFacing,self.photoModel,0) end
    self:ApplyPhotoModelPosition(true); self:ApplyPhotoVisuals(true); self:SetPhotoAnimation(0,"Idle")
end
function T:SavePhotoPreset(slot)
    if not self.db or not self.db.profile then return end
    local ph=self.db.profile.photo or {}; self.db.profile.photo=ph; ph.presets=ph.presets or {}
    ph.presets[slot]={background=self.state.photoBackground,effect=self.state.photoEffect,atmosphere=self.state.photoAtmosphere,
        effectIntensity=self.state.photoEffectIntensity,density=self.state.photoDensity,dim=self.state.photoDim,posX=self.state.photoPosX,posY=self.state.photoPosY,zoom=self.state.photoZoom}
    self:Print("Photo preset "..slot.." saved.")
end
function T:LoadPhotoPreset(slot)
    local ph=self.db and self.db.profile and self.db.profile.photo; local pr=ph and ph.presets and ph.presets[slot]
    if not pr then self:Print("Photo preset "..slot.." is empty."); return end
    self.state.photoBackground=pr.background or "studio"; self.state.photoEffect=pr.effect or "none"; self.state.photoAtmosphere=pr.atmosphere or "none"
    self.state.photoEffectIntensity=pr.effectIntensity or .78; self.state.photoDensity=pr.density or "high"; self.state.photoDim=pr.dim or 0; self.state.photoPosX=pr.posX or 0; self.state.photoPosY=pr.posY or 0; self.state.photoZoom=pr.zoom or .78
    self:SetPhotoBackground(self.state.photoBackground,false); self:ApplyPhotoVisuals(false); self:ApplyPhotoModelPosition(false)
    if self.photoModel and self.photoModel.SetCamDistanceScale then pcall(self.photoModel.SetCamDistanceScale,self.photoModel,self.state.photoZoom) end
    self:Print("Photo preset "..slot.." loaded.")
end

function T:SetPhotoBackground(key,save)
    local p=self.photoMode
    local def=self:GetPhotoBackgroundDef(key)
    if not def then return end
    self.state.photoBackground=def.key
    if save and self.db and self.db.profile then
        self.db.profile.photo=self.db.profile.photo or {}
        self.db.profile.photo.background=def.key
    end
    if p and p.bgL and p.bgR then
        if def.solid then
            p.bgL:SetTexture("Interface\\Buttons\\WHITE8X8"); p.bgR:SetTexture("Interface\\Buttons\\WHITE8X8")
            p.bgL:SetVertexColor(.025,.022,.028,1); p.bgR:SetVertexColor(.025,.022,.028,1)
            p.bgL:SetTexCoord(0,1,0,1); p.bgR:SetTexCoord(0,1,0,1)
        else
            local sourceKey=def.sourceKey or def.key
            p.bgL:SetTexture(self.MEDIA.."photo_bg_"..sourceKey.."_left.tga")
            p.bgR:SetTexture(self.MEDIA.."photo_bg_"..sourceKey.."_right.tga")
            p.bgL:SetVertexColor(1,1,1,1); p.bgR:SetVertexColor(1,1,1,1)
            p.bgL:SetTexCoord(0,790/1024,0,880/1024); p.bgR:SetTexCoord(0,790/1024,0,880/1024)
        end
    end
    if p then self:ApplyPhotoVisuals(save) end
    if p and p.backgroundName then p.backgroundName:SetText(def.name) end
    if p and p.backgroundRows then
        for _,row in ipairs(p.backgroundRows) do
            if row.SetBackdropBorderColor then
                if row.bgKey==def.key then row:SetBackdropBorderColor(rgba(C.gold)) else row:SetBackdropBorderColor(rgba(C.gold2)) end
            end
        end
    end
end

function T:TogglePhotoBackgroundSelector()
    if not self.photoMode or not self.photoMode.backgroundPanel then return end
    if self.photoMode.fxPanel then self.photoMode.fxPanel:Hide() end
    local panel=self.photoMode.backgroundPanel
    panel:SetShown(not panel:IsShown())
end

function T:CreatePhotoMode()
    if self.photoMode then return end
    local p=frame("Frame","TransmogginPhotoMode",UIParent); self.photoMode=p
    p:SetAllPoints(UIParent); p:SetFrameStrata("FULLSCREEN_DIALOG"); p:SetFrameLevel(500); p:EnableMouse(true); p:Hide()

    -- Photo backgrounds are authored at 1580x880 and split into two WoW-safe
    -- 1024x1024 tiles. Only the currently selected pair is loaded by the scene.
    local bgL=p:CreateTexture(nil,"BACKGROUND")
    bgL:SetPoint("TOPLEFT"); bgL:SetPoint("BOTTOMLEFT"); bgL:SetWidth(UIParent:GetWidth()/2); bgL:SetTexCoord(0,790/1024,0,880/1024)
    local bgR=p:CreateTexture(nil,"BACKGROUND")
    bgR:SetPoint("TOPRIGHT"); bgR:SetPoint("BOTTOMRIGHT"); bgR:SetWidth(UIParent:GetWidth()/2); bgR:SetTexCoord(0,790/1024,0,880/1024)
    p.bgL,p.bgR=bgL,bgR
    p:SetScript("OnSizeChanged",function(self,w) if self.bgL then self.bgL:SetWidth(w/2); self.bgR:SetWidth(w/2) end end)

    -- Background-only dimmer: no fullscreen color grading over the character.
    local backgroundDim=p:CreateTexture(nil,"BORDER"); backgroundDim:SetAllPoints(); backgroundDim:SetTexture("Interface\\Buttons\\WHITE8X8"); backgroundDim:SetVertexColor(0,0,0); backgroundDim:SetAlpha(0); backgroundDim:Hide(); p.backgroundDim=backgroundDim

    local model=CreateFrame("DressUpModel",nil,p); self.photoModel=model
    -- Compose the character slightly left of center and lower in the scene so
    -- the feet land closer to the foreground medallion and the background's
    -- focal architecture remains visible. Percent-based horizontal anchors
    -- keep the composition consistent across supported resolutions.
    self.state.photoPosX=self.state.photoPosX or 0; self.state.photoPosY=self.state.photoPosY or 0
    model:SetPoint("TOPLEFT",UIParent,"TOPLEFT",UIParent:GetWidth()*(.14+self.state.photoPosX),-80+(UIParent:GetHeight()*self.state.photoPosY))
    model:SetPoint("BOTTOMRIGHT",UIParent,"BOTTOMRIGHT",-UIParent:GetWidth()*(.22-self.state.photoPosX),-20+(UIParent:GetHeight()*self.state.photoPosY))
    model:SetUnit("player"); model:EnableMouse(true); model:EnableMouseWheel(true)
    if model.SetFrameLevel then model:SetFrameLevel(p:GetFrameLevel()+4) end

    local atmoLayer=frame("Frame",nil,p); p.atmoLayer=atmoLayer; atmoLayer:SetAllPoints(p); atmoLayer:SetFrameLevel(p:GetFrameLevel()+2); atmoLayer:EnableMouse(false)
    p.atmoSprites={}
    for i=1,96 do local tex=atmoLayer:CreateTexture(nil,"ARTWORK"); tex:Hide(); p.atmoSprites[i]=tex end
    local fxLayer=frame("Frame",nil,p); p.fxLayer=fxLayer; fxLayer:SetAllPoints(model); fxLayer:SetFrameLevel(p:GetFrameLevel()+6); fxLayer:EnableMouse(false)
    p.fxSprites={}
    for i=1,48 do local tex=fxLayer:CreateTexture(nil,"ARTWORK"); tex:Hide(); p.fxSprites[i]=tex end
    p:SetScript("OnUpdate",function(_,elapsed) T:UpdatePhotoStudioFX(elapsed) end)
    if model.SetCamDistanceScale then pcall(model.SetCamDistanceScale,model,.78) end
    model:SetScript("OnMouseDown",function(m,btn) if btn=="LeftButton" then m.__drag=true; m.__x=GetCursorPosition() end end)
    model:SetScript("OnMouseUp",function(m) m.__drag=false end)
    model:SetScript("OnUpdate",function(m) if m.__drag and m.SetFacing then local x=GetCursorPosition(); local dx=(x-(m.__x or x))/180; m.__x=x; m:SetFacing((m:GetFacing() or 0)+dx) end end)
    model:SetScript("OnMouseWheel",function(m,d)
        if not m.SetCamDistanceScale then return end
        T.state.photoZoom=math.max(.45,math.min(2.20,(T.state.photoZoom or .78)-(d*.08)))
        pcall(m.SetCamDistanceScale,m,T.state.photoZoom)
    end)

    local top=frame("Frame",nil,p); p.topBar=top; top:SetFrameLevel(p:GetFrameLevel()+20); top:SetPoint("TOP",0,-16); top:SetSize(420,62); skin(top,{0.02,0.016,0.013,.90},{0.45,0.24,0.06,1},2)
    local logo=top:CreateTexture(nil,"ARTWORK"); logo:SetTexture(self.ROOSTER); logo:SetSize(50,50); logo:SetPoint("LEFT",12,0)
    local ttl=fs(top,24,C.gold); ttl:SetPoint("LEFT",logo,"RIGHT",10,7); ttl:SetText("PHOTO MODE")
    self.photoPoseName=fs(top,10,C.muted); self.photoPoseName:SetPoint("LEFT",logo,"RIGHT",10,-15); self.photoPoseName:SetText("Idle")

    local c=frame("Frame",nil,p); p.controls=c; c:SetFrameLevel(p:GetFrameLevel()+20); c:SetPoint("RIGHT",-24,0); c:SetSize(280,680); skin(c,{0.025,0.020,0.016,.91},{0.42,0.22,0.06,1},2)
    local ct=fs(c,18,C.gold); ct:SetPoint("TOPLEFT",16,-15); ct:SetText("POSE STUDIO")
    local hint=fs(c,10,C.muted); hint:SetPoint("TOPLEFT",16,-42); hint:SetPoint("RIGHT",-16,0); hint:SetJustifyH("LEFT"); hint:SetText("Choose a pose or emote. Drag the character to rotate. Mouse wheel zooms.")

    local bgButton=button(c,"BACKGROUNDS",96,24); bgButton:SetPoint("TOPRIGHT",-15,-14)
    bgButton:SetScript("OnClick",function() T:TogglePhotoBackgroundSelector() end)
    local fxButton=button(c,"STUDIO FX",82,24); fxButton:SetPoint("RIGHT",bgButton,"LEFT",-6,0)

    local bgPanel=frame("Frame",nil,p); p.backgroundPanel=bgPanel; bgPanel:SetFrameLevel(p:GetFrameLevel()+24)
    bgPanel:SetSize(570,535); bgPanel:SetPoint("TOPLEFT",p,"TOPLEFT",96,-105); skin(bgPanel,{0.018,0.015,0.013,.97},{0.45,0.24,0.06,1},2); bgPanel:Hide()
    local bgt=fs(bgPanel,18,C.gold); bgt:SetPoint("TOPLEFT",14,-13); bgt:SetText("PHOTO BACKGROUNDS")
    local current=fs(bgPanel,10,C.muted); p.backgroundName=current; current:SetPoint("TOPLEFT",14,-34); current:SetText("")
    local close=button(bgPanel,"X",26,24); close:SetPoint("TOPRIGHT",-8,-8); close:SetScript("OnClick",function() bgPanel:Hide() end)
    p.backgroundRows={}
    for i,bg in ipairs(self.PHOTO_BACKGROUNDS or {}) do
        local col=(i-1)%3; local row=math.floor((i-1)/3)
        local b=frame("Button",nil,bgPanel); b:SetSize(170,70); b:SetPoint("TOPLEFT",14+col*182,-52-row*77); skin(b,{0.028,0.022,0.018,.96},C.gold2,1); b.bgKey=bg.key
        local tex=b:CreateTexture(nil,"ARTWORK"); tex:SetPoint("TOPLEFT",3,-3); tex:SetPoint("BOTTOMRIGHT",-3,19)
        if bg.solid then tex:SetTexture("Interface\\Buttons\\WHITE8X8"); tex:SetVertexColor(.025,.022,.028,1); tex:SetTexCoord(0,1,0,1)
        else tex:SetTexture(T.MEDIA.."photo_bg_"..(bg.sourceKey or bg.key).."_thumb.tga"); tex:SetTexCoord(0,1,56/256,200/256) end
        local label=fs(b,9,C.text); label:SetPoint("BOTTOMLEFT",4,3); label:SetPoint("BOTTOMRIGHT",-4,3); label:SetJustifyH("CENTER"); label:SetText("["..(bg.category or "SCENE").."] "..bg.name)
        b:SetScript("OnClick",function() T:SetPhotoBackground(bg.key,true); bgPanel:Hide() end)
        b:SetScript("OnEnter",function(self) if self.SetBackdropBorderColor then self:SetBackdropBorderColor(rgba(C.gold)) end end)
        b:SetScript("OnLeave",function(self) if self.SetBackdropBorderColor then if T.state.photoBackground==self.bgKey then self:SetBackdropBorderColor(rgba(C.gold)) else self:SetBackdropBorderColor(rgba(C.gold2)) end end end)
        table.insert(p.backgroundRows,b)
    end

    local fxPanel=frame("Frame",nil,p); p.fxPanel=fxPanel; fxPanel:SetFrameLevel(p:GetFrameLevel()+24)
    fxPanel:SetSize(340,660); fxPanel:SetPoint("TOPLEFT",p,"TOPLEFT",96,-105); skin(fxPanel,{0.018,0.015,0.013,.97},{0.45,0.24,0.06,1},2); fxPanel:Hide()
    local fxt=fs(fxPanel,18,C.gold); fxt:SetPoint("TOPLEFT",14,-13); fxt:SetText("STUDIO FX")
    local fxSub=fs(fxPanel,9,C.muted); fxSub:SetPoint("TOPLEFT",14,-36); fxSub:SetText("Localized character FX + lightweight atmosphere")
    local fxClose=button(fxPanel,"X",26,24); fxClose:SetPoint("TOPRIGHT",-8,-8); fxClose:SetScript("OnClick",function() fxPanel:Hide() end)
    local auraTitle=fs(fxPanel,11,C.gold); auraTitle:SetPoint("TOPLEFT",14,-60); auraTitle:SetText("CHARACTER FX")
    for i,def in ipairs(self.PHOTO_EFFECTS or {}) do
        local col=(i-1)%3; local row=math.floor((i-1)/3)
        local b=button(fxPanel,def.name,96,24); b:SetPoint("TOPLEFT",14+col*103,-77-row*29)
        b:SetScript("OnClick",function() T:SetPhotoEffect(def.key,true) end)
    end
    local atTitle=fs(fxPanel,11,C.gold); atTitle:SetPoint("TOPLEFT",14,-260); atTitle:SetText("ATMOSPHERE")
    for i,def in ipairs(self.PHOTO_ATMOSPHERES or {}) do
        local col=(i-1)%3; local row=math.floor((i-1)/3)
        local b=button(fxPanel,def.name,96,24); b:SetPoint("TOPLEFT",14+col*103,-277-row*29)
        b:SetScript("OnClick",function() T:SetPhotoAtmosphere(def.key,true) end)
    end
    local densTitle=fs(fxPanel,11,C.gold); densTitle:SetPoint("TOPLEFT",14,-390); densTitle:SetText("DENSITY")
    for i,def in ipairs(self.PHOTO_DENSITIES or {}) do
        local b=button(fxPanel,def.name,72,23); b:SetPoint("TOPLEFT",14+(i-1)*77,-407); b:SetScript("OnClick",function() T:SetPhotoDensity(def.key,true) end)
    end
    local status=fs(fxPanel,9,C.muted); p.fxStatus=status; status:SetPoint("TOPLEFT",14,-442); status:SetPoint("RIGHT",-14,0); status:SetJustifyH("LEFT")
    local intM=button(fxPanel,"FX -",70,24); intM:SetPoint("TOPLEFT",14,-462); intM:SetScript("OnClick",function() T:AdjustPhotoEffectIntensity(-.10) end)
    local intP=button(fxPanel,"FX +",70,24); intP:SetPoint("LEFT",intM,"RIGHT",6,0); intP:SetScript("OnClick",function() T:AdjustPhotoEffectIntensity(.10) end)
    local dimM=button(fxPanel,"BG -",70,24); dimM:SetPoint("LEFT",intP,"RIGHT",6,0); dimM:SetScript("OnClick",function() T:AdjustPhotoDim(-.05) end)
    local dimP=button(fxPanel,"BG +",70,24); dimP:SetPoint("LEFT",dimM,"RIGHT",6,0); dimP:SetScript("OnClick",function() T:AdjustPhotoDim(.05) end)
    local posTitle=fs(fxPanel,10,C.gold); posTitle:SetPoint("TOPLEFT",14,-496); posTitle:SetText("COMPOSITION")
    local left=button(fxPanel,"LEFT",68,23); left:SetPoint("TOPLEFT",14,-512); left:SetScript("OnClick",function() T:NudgePhotoModel(-.02,0) end)
    local right=button(fxPanel,"RIGHT",68,23); right:SetPoint("LEFT",left,"RIGHT",5,0); right:SetScript("OnClick",function() T:NudgePhotoModel(.02,0) end)
    local up=button(fxPanel,"UP",68,23); up:SetPoint("LEFT",right,"RIGHT",5,0); up:SetScript("OnClick",function() T:NudgePhotoModel(0,.02) end)
    local down=button(fxPanel,"DOWN",68,23); down:SetPoint("LEFT",up,"RIGHT",5,0); down:SetScript("OnClick",function() T:NudgePhotoModel(0,-.02) end)
    local resetFx=button(fxPanel,"RESET",150,24); resetFx:SetPoint("BOTTOM",fxPanel,"BOTTOM",0,12); resetFx:SetScript("OnClick",function() T:ResetPhotoComposition() end)
    fxButton:SetScript("OnClick",function() bgPanel:Hide(); fxPanel:SetShown(not fxPanel:IsShown()) end)

    local y=-105
    for i,a in ipairs(self.PHOTO_ANIMATIONS) do
        local b=button(c,a.name,112,27)
        local col=(i-1)%2; local row=math.floor((i-1)/2)
        b:SetPoint("TOPLEFT",15+col*122,-105-row*33)
        b:SetScript("OnClick",function() T:SetPhotoAnimation(a.id,a.name) end)
    end

    local toolsY=-420
    local sheathe=button(c,"SHEATHE",112,28); sheathe:SetPoint("TOPLEFT",15,toolsY); sheathe:SetScript("OnClick",function() T.state.photoSheathed=true; T:RefreshPhotoModel() end)
    local draw=button(c,"DRAW WEAPONS",112,28); draw:SetPoint("LEFT",sheathe,"RIGHT",10,0); draw:SetScript("OnClick",function() T.state.photoSheathed=false; T:RefreshPhotoModel() end)
    local zIn=button(c,"ZOOM +",112,28); zIn:SetPoint("TOPLEFT",15,toolsY-38); zIn:SetScript("OnClick",function() T.state.photoZoom=math.max(.45,(T.state.photoZoom or .78)-.10); if T.photoModel.SetCamDistanceScale then pcall(T.photoModel.SetCamDistanceScale,T.photoModel,T.state.photoZoom) end end)
    local zOut=button(c,"ZOOM -",112,28); zOut:SetPoint("LEFT",zIn,"RIGHT",10,0); zOut:SetScript("OnClick",function() T.state.photoZoom=math.min(2.20,(T.state.photoZoom or .78)+.10); if T.photoModel.SetCamDistanceScale then pcall(T.photoModel.SetCamDistanceScale,T.photoModel,T.state.photoZoom) end end)
    local reset=button(c,"RESET VIEW",234,28); reset:SetPoint("TOPLEFT",15,toolsY-76); reset:SetScript("OnClick",function() T:ResetPhotoComposition() end)
    local hide=button(c,"HIDE CONTROLS",234,30); hide:SetPoint("TOPLEFT",15,toolsY-116); hide:SetScript("OnClick",function() T:SetPhotoControlsVisible(false) end)
    local capture=button(c,"TAKE PHOTO",234,36); capture:SetPoint("TOPLEFT",15,toolsY-158); skin(capture,{0.19,0.055,0.018,.98},C.gold,2); capture:SetScript("OnClick",function() T:CapturePhoto() end)

    local show=button(p,"SHOW CONTROLS",130,28); p.showControls=show; show:SetPoint("BOTTOMRIGHT",-24,24); show:SetScript("OnClick",function() T:SetPhotoControlsVisible(true) end); show:Hide()
    c:HookScript("OnHide",function() if p:IsShown() then show:Show() end end)
    c:HookScript("OnShow",function() show:Hide() end)

    -- Compact Photo Studio dock. The legacy panel remains hidden for compatibility.
    c:Hide(); show:Hide()
    local dock=frame("Frame",nil,p); p.dock=dock; dock:SetFrameLevel(p:GetFrameLevel()+30)
    dock:SetSize(438,48); dock:SetPoint("BOTTOMRIGHT",-20,18)

    local function tip(btn,title,body)
        btn:SetScript("OnEnter",function(self) if T.ShowHelpTooltip then T:ShowHelpTooltip(self,title,body or "","ANCHOR_TOP") end end)
        btn:SetScript("OnLeave",function() if GameTooltip then GameTooltip:Hide() end end)
    end
    local function iconButton(key,tex,title,body)
        local b=frame("Button",nil,dock); b:SetSize(42,42); skin(b,{0.018,0.012,0.010,.88},C.gold2,1)
        local t=b:CreateTexture(nil,"ARTWORK"); t:SetPoint("TOPLEFT",2,-2); t:SetPoint("BOTTOMRIGHT",-2,2); t:SetTexture(T.MEDIA..tex); t:SetTexCoord(.08,.92,.08,.92); b.icon=t
        tip(b,title,body); return b
    end
    local dockDefs={
        {"capture","photo_take.tga","Take Photo","Capture a clean screenshot."},
        {"pose","photo_pose.tga","Pose Studio","Choose a pose or emote."},
        {"fx","photo_fx.tga","Studio FX","Character effects and atmosphere."},
        {"bg","photo_background.tga","Backgrounds","Choose the Photo Mode scene."},
        {"camera","photo_camera.tga","Camera Controls","Zoom, position, weapons and reset."},
        {"preset","photo_presets.tga","Presets","Save or load Photo Studio presets."},
        {"world","photo.tga","World Selfie","Use the live game world and hide HUD/nameplate clutter. Other world characters remain if present."},
        {"hide","photo_hide.tga","Hide UI","Hide Photo Mode controls for a clean view."},
        {"exit","photo_exit.tga","Exit Photo Mode","Return to Transmoggin."},
    }
    local dockButtons={}
    for i,d in ipairs(dockDefs) do local b=iconButton(d[1],d[2],d[3],d[4]); b:SetPoint("LEFT",(i-1)*48,0); dockButtons[d[1]]=b end

    local activePanel
    local function closePanels()
        if activePanel then activePanel:Hide(); activePanel=nil end
        bgPanel:Hide(); fxPanel:Hide()
    end
    local function miniPanel(w,h)
        local f=frame("Frame",nil,p); f:SetFrameLevel(p:GetFrameLevel()+29); f:SetSize(w,h); f:SetPoint("BOTTOMRIGHT",p,"BOTTOMRIGHT",-20,72); skin(f,{0.018,0.015,0.013,.94},{0.45,0.24,0.06,1},2); f:Hide(); return f
    end
    local function togglePanel(f)
        local was=f:IsShown(); closePanels(); if not was then f:Show(); activePanel=f end
    end

    local posePanel=miniPanel(250,330); p.posePanel=posePanel
    local pt=fs(posePanel,14,C.gold); pt:SetPoint("TOPLEFT",12,-10); pt:SetText("POSE STUDIO")
    for i,a in ipairs(self.PHOTO_ANIMATIONS) do
        local col=(i-1)%2; local row=math.floor((i-1)/2); local b=button(posePanel,a.name,108,24); b:SetPoint("TOPLEFT",12+col*116,-36-row*28); b:SetScript("OnClick",function() T:SetPhotoAnimation(a.id,a.name) end)
    end

    -- Reuse the existing FX/background panels and anchor them beside the dock.
    fxPanel:ClearAllPoints(); fxPanel:SetPoint("BOTTOMRIGHT",p,"BOTTOMRIGHT",-20,72)
    bgPanel:ClearAllPoints(); bgPanel:SetPoint("BOTTOMRIGHT",p,"BOTTOMRIGHT",-20,72)

    local camPanel=miniPanel(250,180); p.cameraPanel=camPanel
    local camt=fs(camPanel,14,C.gold); camt:SetPoint("TOPLEFT",12,-10); camt:SetText("CAMERA")
    local ci=button(camPanel,"ZOOM +",104,25); ci:SetPoint("TOPLEFT",12,-38); ci:SetScript("OnClick",function() T.state.photoZoom=math.max(.45,(T.state.photoZoom or .78)-.10); if T.photoModel.SetCamDistanceScale then pcall(T.photoModel.SetCamDistanceScale,T.photoModel,T.state.photoZoom) end end)
    local co=button(camPanel,"ZOOM -",104,25); co:SetPoint("LEFT",ci,"RIGHT",8,0); co:SetScript("OnClick",function() T.state.photoZoom=math.min(2.20,(T.state.photoZoom or .78)+.10); if T.photoModel.SetCamDistanceScale then pcall(T.photoModel.SetCamDistanceScale,T.photoModel,T.state.photoZoom) end end)
    local l=button(camPanel,"LEFT",50,24); l:SetPoint("TOPLEFT",12,-72); l:SetScript("OnClick",function() T:NudgePhotoModel(-.02,0) end)
    local r=button(camPanel,"RIGHT",50,24); r:SetPoint("LEFT",l,"RIGHT",5,0); r:SetScript("OnClick",function() T:NudgePhotoModel(.02,0) end)
    local u=button(camPanel,"UP",50,24); u:SetPoint("LEFT",r,"RIGHT",5,0); u:SetScript("OnClick",function() T:NudgePhotoModel(0,.02) end)
    local d=button(camPanel,"DOWN",50,24); d:SetPoint("LEFT",u,"RIGHT",5,0); d:SetScript("OnClick",function() T:NudgePhotoModel(0,-.02) end)
    local sh=button(camPanel,"SHEATHE",104,24); sh:SetPoint("TOPLEFT",12,-106); sh:SetScript("OnClick",function() T.state.photoSheathed=true; T:RefreshPhotoModel() end)
    local dr=button(camPanel,"DRAW",104,24); dr:SetPoint("LEFT",sh,"RIGHT",8,0); dr:SetScript("OnClick",function() T.state.photoSheathed=false; T:RefreshPhotoModel() end)
    local rs=button(camPanel,"RESET VIEW",216,24); rs:SetPoint("TOPLEFT",12,-140); rs:SetScript("OnClick",function() T:ResetPhotoComposition() end)

    local prePanel=miniPanel(250,120); p.presetPanel=prePanel
    local prt=fs(prePanel,14,C.gold); prt:SetPoint("TOPLEFT",12,-10); prt:SetText("PHOTO PRESETS")
    for i=1,3 do local sv=button(prePanel,"SAVE "..i,68,25); sv:SetPoint("TOPLEFT",12+(i-1)*76,-38); sv:SetScript("OnClick",function() T:SavePhotoPreset(i) end); local ld=button(prePanel,"LOAD "..i,68,25); ld:SetPoint("TOPLEFT",12+(i-1)*76,-72); ld:SetScript("OnClick",function() T:LoadPhotoPreset(i) end) end

    dockButtons.capture:SetScript("OnClick",function() closePanels(); T:CapturePhoto() end)
    dockButtons.pose:SetScript("OnClick",function() togglePanel(posePanel) end)
    dockButtons.fx:SetScript("OnClick",function() local was=fxPanel:IsShown(); closePanels(); if not was then fxPanel:Show(); activePanel=fxPanel end end)
    dockButtons.bg:SetScript("OnClick",function() local was=bgPanel:IsShown(); closePanels(); if not was then bgPanel:Show(); activePanel=bgPanel end end)
    dockButtons.camera:SetScript("OnClick",function() togglePanel(camPanel) end)
    dockButtons.preset:SetScript("OnClick",function() togglePanel(prePanel) end)
    dockButtons.world:SetScript("OnClick",function() closePanels(); T:EnterWorldSelfieMode() end)
    dockButtons.hide:SetScript("OnClick",function() closePanels(); T:SetPhotoControlsVisible(false); show:Show() end)
    dockButtons.exit:SetScript("OnClick",function() T:TogglePhotoMode() end)
    show:ClearAllPoints(); show:SetPoint("BOTTOMRIGHT",-20,18); show:SetSize(42,42); show:SetText(""); show:SetFrameLevel(p:GetFrameLevel()+31)
    local si=show:CreateTexture(nil,"ARTWORK"); si:SetPoint("TOPLEFT",2,-2); si:SetPoint("BOTTOMRIGHT",-2,2); si:SetTexture(T.MEDIA.."photo_hide.tga"); si:SetTexCoord(.08,.92,.08,.92)
    show:SetScript("OnClick",function() T:SetPhotoControlsVisible(true); show:Hide() end)

    local ph=(self.db and self.db.profile and self.db.profile.photo) or {}
    self.state.photoAnimation=0; self.state.photoZoom=ph.zoom or .78; self.state.photoControlsVisible=true; self.state.photoSheathed=true
    self.state.photoEffect=ph.effect or "none"; self.state.photoAtmosphere=ph.atmosphere or "none"; self.state.photoEffectIntensity=ph.effectIntensity or .78; self.state.photoDensity=ph.density or "high"; self.state.photoDim=ph.dim or 0
    self.state.photoPosX=ph.posX or 0; self.state.photoPosY=ph.posY or 0
    local savedBG=ph.background or "studio"
    self:SetPhotoBackground(savedBG,false); self:ApplyPhotoModelPosition(false); self:ApplyPhotoVisuals(false)
end

function T:EnterPhotoMode()
    self:EnsureUI(); if not self.photoMode then self:CreatePhotoMode() end
    if not self.photoMode then return end
    self:StopAllPhotoFX()
    self.state.worldSelfie=false
    if self.worldSelfieControls then self.worldSelfieControls:Hide() end
    self.state.photoMode=true
    if self.main then self.main:Hide() end
    if self.tracker then self.tracker:Hide() end
    -- A transparent Blizzard transmog frame can still leak its model viewports
    -- on Classic while a vendor session is active. Keep the session alive but
    -- isolate the stock frame for the duration of Photo Mode.
    if self.state.vendorOpen and self.SetPhotoVendorIsolation then self:SetPhotoVendorIsolation(true) end
    self.photoMode:Show(); self:SetPhotoControlsVisible(true); self:RefreshPhotoModel(); self:SetPhotoAnimation(self.state.photoAnimation or 0,"Idle")
end

function T:ExitPhotoMode()
    if self.state and self.state.worldSelfie then self:ExitWorldSelfieMode() end
    self.state.photoMode=false
    self:StopAllPhotoFX()
    if self.photoMode then self.photoMode:Hide() end
    if self.SetPhotoVendorIsolation then self:SetPhotoVendorIsolation(false) end
    if self.main then self.main:Show(); if self.RefreshAll then self:RefreshAll() end end
end

function T:TogglePhotoMode()
    if self.state.photoMode then self:ExitPhotoMode() else self:EnterPhotoMode() end
end
