local ADDON_NAME, RPAL = ...

-- ============================================================
-- ODYN Persona Observatory v1
-- Large, fixed-theme, read-only Persona surface. Calibration is the only
-- manual authoring step; afterward this screen is observation only.
-- ============================================================

local P={frame=nil,built=false,traitRows={},likeRows={},dislikeRows={},peopleRows={},changeRows={},frequencyButtons={},stars={},memoryRows={},insightRows={},turningRows={},threadRows={},interpretationButtons={}}
local introFrame
RPAL.PersonalityUI=P

local WHITE="Interface\\Buttons\\WHITE8X8"
local FONT=STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF"
local C={
    bg={.008,.008,.020,1},deep={.014,.012,.032,1},panel={.025,.020,.047,.985},panel2={.038,.029,.066,.985},
    border={.30,.22,.48,.92},border2={.20,.17,.34,.78},gold={.91,.71,.35,1},pale={.92,.93,.97,1},muted={.57,.59,.68,1},
    teal={.32,.79,.77,1},green={.29,.84,.51,1},red={.94,.36,.41,1},violet={.50,.36,.82,1},gray={.46,.48,.56,1}
}

local function clamp(v,lo,hi)v=tonumber(v)or lo;if v<lo then return lo elseif v>hi then return hi end;return v end
local function backdrop(f,bg,border)if not f or not f.SetBackdrop then return end;f:SetBackdrop({bgFile=WHITE,edgeFile=WHITE,edgeSize=1});f:SetBackdropColor(bg[1],bg[2],bg[3],bg[4]or 1);f:SetBackdropBorderColor(border[1],border[2],border[3],border[4]or 1)end
local function font(parent,value,size,color)
    local fs=parent:CreateFontString(nil,"OVERLAY");fs:SetFont(FONT,size or 10,"");fs:SetText(tostring(value or""));color=color or C.pale;fs:SetTextColor(color[1],color[2],color[3],color[4]or 1)
    if RPAL.RegisterScalableFontObject then RPAL.RegisterScalableFontObject(fs,FONT,size or 10,"")end
    return fs
end
local function line(parent,x1,y1,x2,y2,color,alpha)
    local t=parent:CreateTexture(nil,"ARTWORK");t:SetTexture(WHITE);local dx,dy=x2-x1,y2-y1;local len=math.sqrt(dx*dx+dy*dy);t:SetSize(len,1.25);t:SetPoint("CENTER",parent,"TOPLEFT",(x1+x2)/2,-((y1+y2)/2));local a=0;if math.atan2 then a=math.atan2(-dy,dx)else a=math.atan((-dy)/(dx==0 and .0001 or dx));if dx<0 then a=a+math.pi end end;t:SetRotation(a);color=color or C.border;t:SetVertexColor(color[1],color[2],color[3],alpha or .75);return t
end
local function button(parent,label,w,h)
    local b=CreateFrame("Button",nil,parent,"BackdropTemplate");b:SetSize(w or 100,h or 28);backdrop(b,C.panel2,C.border);b.label=font(b,label,10.4,C.pale);b.label:SetPoint("CENTER")
    b:SetScript("OnEnter",function(self)self:SetBackdropBorderColor(C.gold[1],C.gold[2],C.gold[3],1)end);b:SetScript("OnLeave",function(self)self:SetBackdropBorderColor(C.border[1],C.border[2],C.border[3],C.border[4])end);return b
end
local function card(parent,w,h,x,y)
    local f=CreateFrame("Frame",nil,parent,"BackdropTemplate");f:SetSize(w,h);f:SetPoint("TOPLEFT",x,-y);backdrop(f,C.panel,C.border2);return f
end
local function cardTitle(f,title,sub)
    local t=font(f,title,12.6,C.gold);t:SetPoint("TOPLEFT",14,-12)
    if sub then local s=font(f,sub,9.0,C.muted);s:SetPoint("TOPLEFT",14,-32);s:SetPoint("TOPRIGHT",-14,-32);s:SetWordWrap(true)end
end
local function effectiveAlignment()
    return(RPAL.GetEffectivePersonalityAlignment and RPAL.GetEffectivePersonalityAlignment())or((RPAL.GetPersonalityData and RPAL.GetPersonalityData()or{}).alignment)or{good=.34,neutral=.46,evil=.20}
end

local function makeAmbientStars(f)
    for i=1,18 do
        local t=f:CreateTexture(nil,"BACKGROUND",nil,-2);t:SetTexture(WHITE);local sz=(i%4==0)and 3 or 2;t:SetSize(sz,sz);t:SetPoint("TOPLEFT",28+((i*71)%1160),-(24+((i*113)%710)));t:SetVertexColor(C.teal[1],C.teal[2],C.teal[3],.12+(i%5)*.025);P.stars[#P.stars+1]=t
    end
end

local function buildTraitRow(parent,key,index)
    local pair=RPAL.PERSONALITY_TRAIT_PAIRS and RPAL.PERSONALITY_TRAIT_PAIRS[key];if not pair then return end
    local col=(index-1)%2;local ri=math.floor((index-1)/2);local row=CreateFrame("Frame",nil,parent);row:SetSize(344,57);row:SetPoint("TOPLEFT",14+col*359,-64-ri*59);row.key=key
    row.left=font(row,pair.left,9.5,C.pale);row.left:SetPoint("TOPLEFT",0,0);row.right=font(row,pair.right,9.5,C.pale);row.right:SetPoint("TOPRIGHT",0,0);row.read=font(row,"BALANCED",8.7,C.gold);row.read:SetPoint("TOP",0,-1)
    row.track=CreateFrame("Frame",nil,row,"BackdropTemplate");row.track:SetPoint("TOPLEFT",0,-19);row.track:SetSize(344,13);backdrop(row.track,C.deep,C.border2)
    row.mid=row.track:CreateTexture(nil,"OVERLAY");row.mid:SetTexture(WHITE);row.mid:SetSize(1,13);row.mid:SetPoint("CENTER");row.mid:SetVertexColor(C.gray[1],C.gray[2],C.gray[3],.38)
    row.base=row.track:CreateTexture(nil,"OVERLAY");row.base:SetTexture(WHITE);row.base:SetSize(2,17);row.base:SetVertexColor(C.gray[1],C.gray[2],C.gray[3],.9)
    row.current=row.track:CreateTexture(nil,"OVERLAY");row.current:SetTexture(WHITE);row.current:SetSize(4,19);row.current:SetVertexColor(C.teal[1],C.teal[2],C.teal[3],1)
    row.glow=row.track:CreateTexture(nil,"OVERLAY");row.glow:SetTexture(WHITE);row.glow:SetSize(10,23);row.glow:SetBlendMode("ADD");row.glow:SetVertexColor(C.teal[1],C.teal[2],C.teal[3],.18)
    row.meta=font(row,"",8.0,C.muted);row.meta:SetPoint("TOP",0,-37)
    P.traitRows[key]=row
end

local function buildReadRow(parent,collection,index,side)
    local r=CreateFrame("Frame",nil,parent,"BackdropTemplate");collection[index]=r;r:SetSize(177,29);local x=side=="like"and 14 or 207;r:SetPoint("TOPLEFT",x,-82-(index-1)*33);backdrop(r,C.panel2,side=="like"and C.green or C.red)
    r.name=font(r,"",9.2,C.pale);r.name:SetPoint("LEFT",8,0);r.name:SetWidth(114);r.score=font(r,"",8.0,C.muted);r.score:SetPoint("RIGHT",-7,0);r.score:SetJustifyH("RIGHT");return r
end

local function buildPersonRow(parent,index)
    local r=CreateFrame("Frame",nil,parent,"BackdropTemplate");P.peopleRows[index]=r;r:SetSize(524,31);r:SetPoint("TOPLEFT",14,-68-(index-1)*35);backdrop(r,C.panel2,C.border2)
    r.name=font(r,"",9.5,C.pale);r.name:SetPoint("LEFT",9,0);r.name:SetWidth(200);r.state=font(r,"",8.5,C.muted);r.state:SetPoint("CENTER",48,0);r.meta=font(r,"",8.0,C.muted);r.meta:SetPoint("RIGHT",-8,0);return r
end

local function buildChangeRow(parent,index)
    local r=CreateFrame("Frame",nil,parent,"BackdropTemplate");P.changeRows[index]=r;r:SetSize(548,31);r:SetPoint("TOPLEFT",14,-68-(index-1)*35);backdrop(r,C.panel2,C.border2)
    r.title=font(r,"",8.5,C.gold);r.title:SetPoint("LEFT",8,0);r.title:SetWidth(118);r.detail=font(r,"",8.7,C.pale);r.detail:SetPoint("LEFT",128,0);r.detail:SetWidth(410);r.detail:SetWordWrap(false);return r
end

local function build()
    if P.built then return end;P.built=true
    local f=CreateFrame("Frame","ODYN_PersonaObservatory",UIParent,"BackdropTemplate");P.frame=f;f:SetSize(1240,790);f:SetPoint("CENTER");f:SetFrameStrata("FULLSCREEN_DIALOG");f:SetFrameLevel(3150);f:SetToplevel(true);f:SetClampedToScreen(true);f:EnableMouse(true);f:SetClipsChildren(true);f:Hide();backdrop(f,C.bg,C.border)
    makeAmbientStars(f)
    local top=CreateFrame("Frame",nil,f,"BackdropTemplate");P.top=top;top:SetPoint("TOPLEFT",12,-12);top:SetPoint("TOPRIGHT",-12,-12);top:SetHeight(62);backdrop(top,C.panel,C.border)
    P.title=font(top,"PERSONA BETA  •  0.6.4",22,C.gold);P.title:SetPoint("TOPLEFT",18,-10)
    P.subtitle=font(top,"A living behavioural model — calibrated once, learned through roleplay, then left alone.",10,C.muted);P.subtitle:SetPoint("TOPLEFT",20,-39)
    P.signature=font(top,"",9.2,C.pale);P.signature:SetPoint("RIGHT",-248,0);P.signature:SetWidth(330);P.signature:SetJustifyH("RIGHT")
    P.reset=button(top,"RESET PERSONA BETA",148,30);P.reset:SetPoint("RIGHT",-112,0)
    P.close=button(top,"CLOSE",92,30);P.close:SetPoint("RIGHT",-14,0);P.close:SetScript("OnClick",function()RPAL.ClosePersonalityUI()end)

    local scroll=CreateFrame("ScrollFrame",nil,f,"UIPanelScrollFrameTemplate");P.scroll=scroll;scroll:SetPoint("TOPLEFT",20,-84);scroll:SetPoint("BOTTOMRIGHT",-28,18);if RPAL.SkinScrollFrame then RPAL.SkinScrollFrame(scroll)end
    local inner=CreateFrame("Frame",nil,scroll);P.inner=inner;inner:SetSize(1162,2615);scroll:SetScrollChild(inner)

    -- Moral core -----------------------------------------------------------
    local core=card(inner,352,292,0,0);P.core=core;cardTitle(core,"MORAL CORE","Small marker = calibration. Living core = ODYN's current read.")
    local tri=CreateFrame("Frame",nil,core);P.triangle=tri;tri:SetSize(294,190);tri:SetPoint("TOP",0,-68)
    line(tri,147,9,24,167,C.border,.9);line(tri,24,167,270,167,C.border,.9);line(tri,270,167,147,9,C.border,.9)
    local n=font(tri,"NEUTRAL",9.6,C.pale);n:SetPoint("TOP",0,7);local g=font(tri,"GOOD",9.5,C.green);g:SetPoint("BOTTOMLEFT",1,-1);local e=font(tri,"EVIL",9.5,C.red);e:SetPoint("BOTTOMRIGHT",-1,-1)
    P.baseOrb=CreateFrame("Frame",nil,tri,"BackdropTemplate");P.baseOrb:SetSize(11,11);backdrop(P.baseOrb,C.deep,C.gray)
    P.coreGlow=CreateFrame("Frame",nil,tri,"BackdropTemplate");P.coreGlow:SetSize(33,33);backdrop(P.coreGlow,{C.teal[1],C.teal[2],C.teal[3],.04},{C.teal[1],C.teal[2],C.teal[3],.20})
    P.orb=CreateFrame("Frame",nil,tri,"BackdropTemplate");P.orb:SetSize(21,21);backdrop(P.orb,{.055,.15,.16,1},C.gold);local dot=P.orb:CreateTexture(nil,"OVERLAY");dot:SetTexture(WHITE);dot:SetSize(7,7);dot:SetPoint("CENTER");dot:SetVertexColor(C.teal[1],C.teal[2],C.teal[3],1)
    P.alignText=font(core,"",9.0,C.pale);P.alignText:SetPoint("BOTTOM",0,17)

    -- Learning dashboard --------------------------------------------------
    local learn=card(inner,796,292,366,0);P.learn=learn;cardTitle(learn,"PERSONA BETA LEARNING","ODYN reads only this character's future /say and /emote. Party, whisper, guild and fully parenthetical (OOC) lines never enter the model.")
    P.learnState=font(learn,"NOT CALIBRATED",9.2,C.muted);P.learnState:SetPoint("TOPRIGHT",-15,-14)
    P.confLabel=font(learn,"MODEL CONFIDENCE  0%",9.6,C.pale);P.confLabel:SetPoint("TOPLEFT",16,-82)
    P.confTrack=CreateFrame("Frame",nil,learn,"BackdropTemplate");P.confTrack:SetPoint("TOPLEFT",16,-104);P.confTrack:SetSize(500,15);backdrop(P.confTrack,C.deep,C.border2)
    P.confFill=P.confTrack:CreateTexture(nil,"ARTWORK");P.confFill:SetTexture(WHITE);P.confFill:SetPoint("TOPLEFT",1,-1);P.confFill:SetPoint("BOTTOMLEFT",1,1);P.confFill:SetWidth(1);P.confFill:SetVertexColor(C.teal[1],C.teal[2],C.teal[3],.82);P.confCurrent=0;P.confTarget=0
    P.stats=font(learn,"0 learned lines",9.2,C.pale);P.stats:SetPoint("TOPLEFT",16,-137)
    P.source=font(learn,"INPUT  /SAY + /EMOTE     •     OOC (...) IGNORED",8.6,C.muted);P.source:SetPoint("TOPLEFT",16,-159)
    P.latestTitle=font(learn,"LATEST READ",8.6,C.gold);P.latestTitle:SetPoint("TOPLEFT",16,-192)
    P.latest=font(learn,"Calibrate once to give ODYN a starting point.",9.3,C.pale);P.latest:SetPoint("TOPLEFT",16,-210);P.latest:SetWidth(580);P.latest:SetWordWrap(true)
    P.readOnly=font(learn,"TRAIT CONTROLS: READ-ONLY AFTER CALIBRATION",8.5,C.muted);P.readOnly:SetPoint("BOTTOMLEFT",16,16)
    P.calibrate=button(learn,"CALIBRATE PERSONA",150,29);P.calibrate:SetPoint("BOTTOMRIGHT",-16,14);P.calibrate:SetScript("OnClick",function()if RPAL.OpenPersonalityCalibration then RPAL.OpenPersonalityCalibration()end end)

    -- Traits ---------------------------------------------------------------
    local traits=card(inner,748,421,0,306);P.traits=traits;cardTitle(traits,"TRAIT SPECTRUM","Gray = calibration baseline. Teal = current learned read. These cannot be dragged after calibration.")
    local order={"chaoticLawful","forgivingVindictive","altruisticSelfish","truthfulDeceitful","gentleBrutal","superstitiousRational","renegadeParagon","cautiousImpulsive","asceticBonVivant","valorousSpineless","introvertedExtroverted"};for i,key in ipairs(order)do buildTraitRow(traits,key,i)end

    -- Likes/dislikes -------------------------------------------------------
    local assoc=card(inner,400,421,762,306);P.assoc=assoc;cardTitle(assoc,"LIKES & DISLIKES","Calibration seeds these. Repeated RP can strengthen, soften or reverse them over time.")
    local lh=font(assoc,"LIKES",7.7,C.green);lh:SetPoint("TOPLEFT",17,-65);local dh=font(assoc,"DISLIKES",7.7,C.red);dh:SetPoint("TOPLEFT",210,-65)
    for i=1,9 do buildReadRow(assoc,P.likeRows,i,"like");buildReadRow(assoc,P.dislikeRows,i,"dislike")end
    P.noLikes=font(assoc,"No confident likes yet.",8.7,C.muted);P.noLikes:SetPoint("TOPLEFT",20,-92);P.noDislikes=font(assoc,"No confident dislikes yet.",8.7,C.muted);P.noDislikes:SetPoint("TOPLEFT",213,-92)

    -- People ---------------------------------------------------------------
    local people=card(inner,554,312,0,741);P.people=people;cardTitle(people,"PEOPLE","Long-term social impressions inferred from repeated interaction and value compatibility — never from a single line.")
    for i=1,7 do buildPersonRow(people,i)end
    P.noPeople=font(people,"No stable personal impressions yet.",9.0,C.muted);P.noPeople:SetPoint("TOPLEFT",20,-79)

    -- Changes --------------------------------------------------------------
    local changes=card(inner,594,312,568,741);P.changes=changes;cardTitle(changes,"OBSERVED CHANGES","Only confident shifts are logged here. Persona changes are visible, but not manually steered.")
    for i=1,7 do buildChangeRow(changes,i)end
    P.noChanges=font(changes,"Nothing has shifted yet.",9.0,C.muted);P.noChanges:SetPoint("TOPLEFT",20,-79)

    -- Inner voice + state --------------------------------------------------
    local voice=card(inner,1162,244,0,1067);P.voice=voice;cardTitle(voice,"INNER VOICE","Moral Core controls what tends to matter. Voice and Profanity are read-only calibration baselines; ODYN automatically follows sustained changes in this character's own IC wording, slang and swear cadence. Other players' style is never learned.")
    P.voiceConfig=button(voice,"LIVING HABITS",112,22);P.voiceConfig:SetPoint("TOPLEFT",14,-48);P.voiceConfig.label:SetFont(FONT,9.2,"");P.voiceConfig:SetScript("OnClick",function()if RPAL.OpenPersonaLivingConfig then RPAL.OpenPersonaLivingConfig()end end)
    P.voiceSummary=font(voice,"",9.0,C.muted);P.voiceSummary:SetPoint("LEFT",P.voiceConfig,"RIGHT",10,0);P.voiceSummary:SetWidth(330);P.voiceSummary:SetJustifyH("LEFT")
    P.frequencyLabel=font(voice,"THOUGHT FREQUENCY",8.6,C.gold);P.frequencyLabel:SetPoint("TOPRIGHT",-382,-67)
    local freqDefs={{"RARE","rare"},{"QUIET","quiet"},{"BALANCED","balanced"},{"ACTIVE","active"},{"EXPRESSIVE","expressive"}}
    for i,fd in ipairs(freqDefs)do
        local b=button(voice,fd[1],68,22);P.frequencyButtons[i]=b;b.value=fd[2];b:SetPoint("TOPRIGHT",-14-((5-i)*72),-82)
        b.label:SetFont(FONT,8.4,"")
        b:SetScript("OnClick",function(self)if RPAL.SetPersonalityFrequency then RPAL.SetPersonalityFrequency(self.value)end end)
        b:SetScript("OnEnter",function(self)self:SetBackdropBorderColor(C.gold[1],C.gold[2],C.gold[3],1)end)
        b:SetScript("OnLeave",function()if RPAL.RefreshPersonalityUI then RPAL.RefreshPersonalityUI()end end)
    end
    P.frequencyHint=font(voice,"",8.4,C.muted);P.frequencyHint:SetPoint("TOPRIGHT",-14,-108);P.frequencyHint:SetWidth(356);P.frequencyHint:SetJustifyH("RIGHT")
    P.lastThought=font(voice,"No inner thought has surfaced yet.",11.2,C.pale);P.lastThought:SetPoint("TOPLEFT",30,-78);P.lastThought:SetWidth(690);P.lastThought:SetHeight(70);P.lastThought:SetWordWrap(true);P.lastThought:SetJustifyH("CENTER");P.lastThought:SetJustifyV("MIDDLE")
    P.lastMeta=font(voice,"",8.6,C.muted);P.lastMeta:SetPoint("TOPLEFT",30,-158)
    P.lastThoughtHit=CreateFrame("Frame",nil,voice);P.lastThoughtHit:SetPoint("TOPLEFT",P.lastThought,"TOPLEFT",-8,8);P.lastThoughtHit:SetPoint("BOTTOMRIGHT",P.lastThought,"BOTTOMRIGHT",8,-8);P.lastThoughtHit:EnableMouse(true)
    P.lastThoughtHit:SetScript("OnEnter",function(self)
        local row=self.row;if not row or not GameTooltip then return end
        GameTooltip:SetOwner(self,"ANCHOR_CURSOR");GameTooltip:SetText("WHY THIS THOUGHT?",.91,.71,.35)
        if RPAL.GetPersonaThoughtWhy then
            local w=RPAL.GetPersonaThoughtWhy(row)
            if type(w)=="table" then
                if w.trigger and tostring(w.trigger)~="" then GameTooltip:AddLine(tostring(w.trigger),.93,.94,.98,true) end
                if w.appraisal and tostring(w.appraisal)~="" then GameTooltip:AddDoubleLine("READ AS",string.upper(tostring(w.appraisal)),.61,.63,.72,.33,.79,.77) end
                if w.state and tostring(w.state)~="" then GameTooltip:AddDoubleLine("LIVING STATE",tostring(w.state),.61,.63,.72,.93,.94,.98) end
                if tonumber(w.salience) and tonumber(w.salience)>0 then GameTooltip:AddDoubleLine("CONCERN WEIGHT",tostring(math.floor(tonumber(w.salience)+.5)).." / 100",.61,.63,.72,.93,.94,.98) end
                if tonumber(w.confidence) and tonumber(w.confidence)<.999 then GameTooltip:AddDoubleLine("EVENT CONFIDENCE",tostring(math.floor(tonumber(w.confidence)*100+.5)).."%",.61,.63,.72,.93,.94,.98) end
                if tonumber(w.sarcasm) and tonumber(w.sarcasm)>=.35 then GameTooltip:AddDoubleLine("SARCASM READ",tostring(math.floor(tonumber(w.sarcasm)*100+.5)).."%",.61,.63,.72,.93,.94,.98) end
                if tonumber(w.slangHits) and tonumber(w.slangHits)>0 then GameTooltip:AddDoubleLine("SLANG NORMALIZED",tostring(math.floor(tonumber(w.slangHits))).." signal(s)",.61,.63,.72,.93,.94,.98) end
                if w.evidence and tostring(w.evidence)~="" then GameTooltip:AddLine(" ");GameTooltip:AddLine("EVIDENCE",.91,.71,.35);GameTooltip:AddLine("“"..tostring(w.evidence):sub(1,180).."”",.74,.76,.82,true) end
            else GameTooltip:AddLine("No explanation was recorded for this thought.",.61,.63,.72,true) end
        end
        GameTooltip:Show()
    end)
    P.lastThoughtHit:SetScript("OnLeave",function()if GameTooltip then GameTooltip:Hide()end end)
    P.needs=font(voice,"",9.0,C.pale);P.needs:SetPoint("TOPLEFT",770,-132);P.needs:SetWidth(350);P.needs:SetWordWrap(true)
    P.survival=font(voice,"",8.8,C.muted);P.survival:SetPoint("TOPLEFT",770,-190);P.survival:SetWidth(350);P.survival:SetWordWrap(true)

    -- Living continuity ----------------------------------------------------
    local continuity=card(inner,1162,218,0,1325);P.continuity=continuity;cardTitle(continuity,"LIVING CONTINUITY","Living State, nearby relationship pressure and the latest Session Reflection persist beyond individual thought toasts.")
    P.stateKicker=font(continuity,"LIVING STATE",9.4,C.gold);P.stateKicker:SetPoint("TOPLEFT",18,-58)
    P.stateLabel=font(continuity,"CALM",17,C.teal);P.stateLabel:SetPoint("TOPLEFT",18,-80)
    P.stateDetail=font(continuity,"",10.2,C.pale);P.stateDetail:SetPoint("TOPLEFT",18,-109);P.stateDetail:SetWidth(330);P.stateDetail:SetWordWrap(true)
    P.dynamicKicker=font(continuity,"RELATIONSHIP DYNAMICS",9.4,C.gold);P.dynamicKicker:SetPoint("TOPLEFT",390,-58)
    P.dynamicText=font(continuity,"Nobody nearby is pulling strongly on the Persona.",10.1,C.pale);P.dynamicText:SetPoint("TOPLEFT",390,-82);P.dynamicText:SetWidth(334);P.dynamicText:SetHeight(104);P.dynamicText:SetWordWrap(true);P.dynamicText:SetJustifyV("TOP")
    P.reflectionKicker=font(continuity,"LATEST SESSION REFLECTION",9.4,C.gold);P.reflectionKicker:SetPoint("TOPLEFT",764,-58)
    P.reflectionText=font(continuity,"No Session Reflection yet.",10.1,C.pale);P.reflectionText:SetPoint("TOPLEFT",764,-82);P.reflectionText:SetWidth(368);P.reflectionText:SetHeight(104);P.reflectionText:SetWordWrap(true);P.reflectionText:SetJustifyV("TOP")

    local memories=card(inner,1162,278,0,1557);P.memories=memories;cardTitle(memories,"CHARACTER MEMORY","Evidence-backed Episode Cards keep the observed event separate from Persona's interpretation of the character's response.")
    for i=1,6 do
        local r=CreateFrame("Frame",nil,memories,"BackdropTemplate");P.memoryRows[i]=r;r:SetSize(1126,31);r:SetPoint("TOPLEFT",18,-65-(i-1)*34);backdrop(r,C.panel2,C.border2)
        r.kind=font(r,"",8.7,C.gold);r.kind:SetPoint("LEFT",10,0);r.kind:SetWidth(122);r.text=font(r,"",9.6,C.pale);r.text:SetPoint("LEFT",138,0);r.text:SetWidth(970);r.text:SetWordWrap(false)
    end
    P.noMemories=font(memories,"No evidence-backed Persona memories yet.",10,C.muted);P.noMemories:SetPoint("TOPLEFT",20,-78)

    -- Evidence-backed character model --------------------------------------
    local insights=card(inner,1162,238,0,1849);P.insights=insights;cardTitle(insights,"CHARACTER INSIGHTS","Patterns appear only after at least three independent episodes. They are Persona observations, not fixed character canon.")
    for i=1,4 do
        local r=CreateFrame("Frame",nil,insights,"BackdropTemplate");P.insightRows[i]=r;r:SetSize(1126,35);r:SetPoint("TOPLEFT",18,-68-(i-1)*39);backdrop(r,C.panel2,C.border2)
        r.text=font(r,"",9.4,C.pale);r.text:SetPoint("LEFT",10,0);r.text:SetWidth(890);r.text:SetWordWrap(false)
        r.meta=font(r,"",8.2,C.teal);r.meta:SetPoint("RIGHT",-10,0);r.meta:SetJustifyH("RIGHT")
    end
    P.noInsights=font(insights,"No stable behavioural pattern has enough independent evidence yet.",9.6,C.muted);P.noInsights:SetPoint("TOPLEFT",20,-82)

    local turning=card(inner,574,260,0,2101);P.turning=turning;cardTitle(turning,"TURNING POINTS","High-impact episodes Persona can point back to without rewriting what happened.")
    for i=1,5 do
        local r=CreateFrame("Frame",nil,turning,"BackdropTemplate");P.turningRows[i]=r;r:SetSize(540,32);r:SetPoint("TOPLEFT",17,-67-(i-1)*36);backdrop(r,C.panel2,C.border2)
        r.text=font(r,"",8.9,C.pale);r.text:SetPoint("LEFT",9,0);r.text:SetWidth(420);r.text:SetWordWrap(false)
        r.meta=font(r,"",7.8,C.gold);r.meta:SetPoint("RIGHT",-8,0);r.meta:SetJustifyH("RIGHT")
    end
    P.noTurning=font(turning,"No turning point has enough evidence yet.",9.2,C.muted);P.noTurning:SetPoint("TOPLEFT",20,-80)

    local threads=card(inner,574,260,588,2101);P.threads=threads;cardTitle(threads,"UNRESOLVED THREADS","Important episodes that still carry an unresolved emotional or relational trace.")
    for i=1,5 do
        local r=CreateFrame("Frame",nil,threads,"BackdropTemplate");P.threadRows[i]=r;r:SetSize(540,32);r:SetPoint("TOPLEFT",17,-67-(i-1)*36);backdrop(r,C.panel2,C.border2)
        r.text=font(r,"",8.9,C.pale);r.text:SetPoint("LEFT",9,0);r.text:SetWidth(405);r.text:SetWordWrap(false)
        r.meta=font(r,"",7.8,C.red);r.meta:SetPoint("RIGHT",-8,0);r.meta:SetJustifyH("RIGHT")
    end
    P.noThreads=font(threads,"Nothing important is currently marked unresolved.",9.2,C.muted);P.noThreads:SetPoint("TOPLEFT",20,-80)

    local interpretation=card(inner,1162,226,0,2375);P.interpretation=interpretation;cardTitle(interpretation,"INTERPRETATION CHECK","Only high-importance, unresolved episodes with an uncertain inner motive can ask once. Your answer clarifies that episode; it does not manually steer the whole Persona.")
    P.interpretationQuestion=font(interpretation,"No ambiguous major episode needs your input.",10.2,C.pale);P.interpretationQuestion:SetPoint("TOPLEFT",22,-68);P.interpretationQuestion:SetPoint("TOPRIGHT",-22,-68);P.interpretationQuestion:SetHeight(58);P.interpretationQuestion:SetWordWrap(true);P.interpretationQuestion:SetJustifyV("TOP")
    local checks={{"FEAR","fear"},{"DISCOMFORT","discomfort"},{"OVERWHELMED","overwhelmed"},{"HIDING WARMTH","hidden-warmth"},{"NOT WHAT HAPPENED","wrong"}}
    for i,def in ipairs(checks)do
        local b=button(interpretation,def[1],208,31);P.interpretationButtons[i]=b;b.choiceID=def[2];b:SetPoint("BOTTOMLEFT",22+(i-1)*222,22);b.label:SetFont(FONT,i==5 and 8.4 or 9.1,"")
        b:SetScript("OnClick",function(self)
            local check=P.currentInterpretationCheck
            if check and RPAL.ResolvePersonaInterpretationCheck and RPAL.ResolvePersonaInterpretationCheck(check.eventID,self.choiceID)then if RPAL.RefreshPersonalityUI then RPAL.RefreshPersonalityUI()end end
        end)
    end

    if StaticPopupDialogs and not StaticPopupDialogs["ODYN_RESET_PERSONA"] then
        StaticPopupDialogs["ODYN_RESET_PERSONA"]={text="Reset Persona Beta for this character?\n\nCalibration, learned traits, Moral Core observations, thought history, Voice & Habits, Living State, Character Memory, Character Insights, Turning Points, Unresolved Threads, interpretation checks, Relationship Dynamics and Session Reflections will be permanently cleared. Other ODYN character data and Bonds are not deleted.",button1="RESET PERSONA BETA",button2="CANCEL",timeout=0,whileDead=true,hideOnEscape=true,preferredIndex=3,OnAccept=function()
            if RPAL.ResetPersonaCompletely then RPAL.ResetPersonaCompletely()end
            if RPAL.ClosePersonalityUI then RPAL.ClosePersonalityUI(true)end
            if C_Timer and C_Timer.After then C_Timer.After(.08,function()if RPAL.OpenPersonalityUI then RPAL.OpenPersonalityUI()end end)elseif RPAL.OpenPersonalityUI then RPAL.OpenPersonalityUI()end
        end}
    end
    P.reset:SetScript("OnClick",function()if StaticPopup_Show then StaticPopup_Show("ODYN_RESET_PERSONA")elseif RPAL.ResetPersonaCompletely then RPAL.ResetPersonaCompletely();RPAL.ClosePersonalityUI(true);RPAL.OpenPersonalityUI()end end)
    if UISpecialFrames then local found=false;for _,n in ipairs(UISpecialFrames)do if n=="ODYN_PersonaObservatory"then found=true break end end;if not found then table.insert(UISpecialFrames,"ODYN_PersonaObservatory")end end

    -- animation ------------------------------------------------------------
    f:SetScript("OnUpdate",function(self,elapsed)
        if not self:IsShown() then return end
        P.anim=(P.anim or 0)+elapsed
        if P.coreGlow and P.orb and P.orb:IsShown() then
            local wave=(math.sin(P.anim*2.2)+1)/2;local size=28+wave*9;P.coreGlow:SetSize(size,size);P.coreGlow:SetAlpha(.22+wave*.18)
        end
        if P.confFill then
            P.confCurrent=P.confCurrent+((P.confTarget-P.confCurrent)*math.min(1,elapsed*5.5));P.confFill:SetWidth(math.max(1,498*(P.confCurrent/100)))
        end
        for i,t in ipairs(P.stars)do t:SetAlpha(.06+((math.sin(P.anim*(.55+(i%4)*.11)+i)+1)/2)*.16)end
    end)
end

local function updateCore(d)
    local eff=effectiveAlignment();local base=d.alignment or eff;local w,h=P.triangle:GetWidth(),P.triangle:GetHeight();local x,y=RPAL.PersonalityWeightsToPoint(eff.good,eff.neutral,eff.evil);P.orb:ClearAllPoints();P.orb:SetPoint("CENTER",P.triangle,"TOPLEFT",x*w,-y*h);P.coreGlow:ClearAllPoints();P.coreGlow:SetPoint("CENTER",P.orb,"CENTER")
    local bx,by=RPAL.PersonalityWeightsToPoint(base.good,base.neutral,base.evil);P.baseOrb:ClearAllPoints();P.baseOrb:SetPoint("CENTER",P.triangle,"TOPLEFT",bx*w,-by*h)
    P.alignText:SetText(string.format("GOOD %.1f%%   •   NEUTRAL %.1f%%   •   EVIL %.1f%%",(eff.good or 0)*100,(eff.neutral or 0)*100,(eff.evil or 0)*100))
end

local function updateTraits(d)
    for key,row in pairs(P.traitRows)do
        local pair=RPAL.PERSONALITY_TRAIT_PAIRS[key];local base=clamp(d.traits and d.traits[key]or 50,0,100);local eff=clamp((RPAL.GetEffectivePersonalityTrait and RPAL.GetEffectivePersonalityTrait(key))or base,0,100);local obs=RPAL.GetPersonalityTraitObservation and RPAL.GetPersonalityTraitObservation(key)or{confidence=0,score=50}
        local width=342;local bx=1+width*(base/100);local ex=1+width*(eff/100);row.base:ClearAllPoints();row.base:SetPoint("CENTER",row.track,"LEFT",bx,0);row.current:ClearAllPoints();row.current:SetPoint("CENTER",row.track,"LEFT",ex,0);row.glow:ClearAllPoints();row.glow:SetPoint("CENTER",row.track,"LEFT",ex,0)
        local strength=math.abs(eff-50)*2;if eff<=44 then row.read:SetText(string.upper(pair.left).." "..math.floor(strength+.5).."%")elseif eff>=56 then row.read:SetText(string.upper(pair.right).." "..math.floor(strength+.5).."%")else row.read:SetText("BALANCED")end
        row.meta:SetText(string.format("CAL %d   •   ODYN %d   •   CONF %d%%",math.floor(base+.5),math.floor(eff+.5),math.floor((obs.confidence or 0)*100+.5)))
    end
end

local function updateAssociations()
    local rows=RPAL.GetPersonalityTopAssociations and RPAL.GetPersonalityTopAssociations(30,false)or{};local likes,dislikes={},{}
    for _,r in ipairs(rows)do if r.score>.12 then likes[#likes+1]=r elseif r.score<-.12 then dislikes[#dislikes+1]=r end end
    table.sort(likes,function(a,b)return a.score*a.confidence>b.score*b.confidence end);table.sort(dislikes,function(a,b)return math.abs(a.score)*a.confidence>math.abs(b.score)*b.confidence end)
    for i=1,9 do
        local a=likes[i];local r=P.likeRows[i];r:SetShown(a~=nil);if a then r.name:SetText(a.display);r.score:SetText(math.floor((a.confidence or 0)*100+.5).."%")end
        local b=dislikes[i];local q=P.dislikeRows[i];q:SetShown(b~=nil);if b then q.name:SetText(b.display);q.score:SetText(math.floor((b.confidence or 0)*100+.5).."%")end
    end
    P.noLikes:SetShown(#likes==0);P.noDislikes:SetShown(#dislikes==0)
end

local function updatePeople()
    local list=RPAL.GetPersonaPeople and RPAL.GetPersonaPeople(7)or{};P.noPeople:SetShown(#list==0)
    for i=1,7 do local r=P.peopleRows[i];local p=list[i];r:SetShown(p~=nil);if p then
        r.name:SetText(p.display or p.key);local score=p.score or 0;local state=(score>=.62 and"FOND")or(score>=.28 and"POSITIVE")or(score<=-.62 and"AVERSE")or(score<=-.28 and"NEGATIVE")or"UNCERTAIN";r.state:SetText(state);r.meta:SetText(string.format("CONF %d%%  •  %d interactions",math.floor((p.confidence or 0)*100+.5),tonumber(p.count)or 0));local col=score>=.28 and C.green or(score<=-.28 and C.red or C.border2);r:SetBackdropBorderColor(col[1],col[2],col[3],col[4]or 1);r.state:SetTextColor(col[1],col[2],col[3],1)
    end end
end

local function updateChanges()
    local list=RPAL.GetPersonalityLearningChanges and RPAL.GetPersonalityLearningChanges(7)or{};P.noChanges:SetShown(#list==0)
    for i=1,7 do local r=P.changeRows[i];local row=list[#list-i+1];r:SetShown(row~=nil);if row then r.title:SetText(string.upper(tostring(row.title or"LEARNED")));r.detail:SetText(tostring(row.detail or""))end end
end

local function updateLearning(d)
    local sm=RPAL.GetPersonalityLearningSummary and RPAL.GetPersonalityLearningSummary()or{};local enabled=not RPAL.IsPersonaSystemEnabled or RPAL.IsPersonaSystemEnabled();local cal=sm.calibrated==true
    P.learnState:SetText(not enabled and"DISABLED"or(cal and"LEARNING"or"NOT CALIBRATED"));local col=not enabled and C.red or(cal and C.teal or C.muted);P.learnState:SetTextColor(col[1],col[2],col[3],1)
    P.confTarget=enabled and(sm.confidence or 0)or 0;P.confLabel:SetText("MODEL CONFIDENCE  "..tostring(math.floor(P.confTarget+.5)).."%");P.stats:SetText(string.format("%d learned lines   •   %d /say   •   %d /emote",sm.messages or 0,sm.say or 0,sm.emote or 0))
    local changes=RPAL.GetPersonalityLearningChanges and RPAL.GetPersonalityLearningChanges(1)or{};P.latest:SetText(changes[1]and((changes[1].title or"Learned").." — "..(changes[1].detail or""))or(cal and"Listening for repeated behaviour. One dramatic line is never enough."or"Calibrate once to give ODYN a starting point."))
    P.calibrate:SetShown(enabled and not cal);P.readOnly:SetText(not enabled and"PERSONA BETA DISABLED IN SETTINGS"or(cal and"CALIBRATED • MODEL READ-ONLY • THOUGHT CADENCE IS YOURS"or"CALIBRATION REQUIRED"))
end

local function updateVoice(d)
    local hist=RPAL.GetPersonalityHistory and RPAL.GetPersonalityHistory(1)or{};local row=hist[#hist]
    if row then P.lastThought:SetText("“"..tostring(row.text or"").."”");P.lastMeta:SetText(string.upper(tostring(row.source or"PERSONA BETA")).."  •  "..string.upper(tostring(row.category or"THOUGHT")).."  •  HOVER THOUGHT FOR WHY");if P.lastThoughtHit then P.lastThoughtHit.row=row end else P.lastThought:SetText("No inner thought has surfaced yet.");P.lastMeta:SetText("");if P.lastThoughtHit then P.lastThoughtHit.row=nil end end
    local freq=tostring((d.autonomy and d.autonomy.frequency)or"balanced")
    for _,b in ipairs(P.frequencyButtons or{})do
        local active=b.value==freq
        if active then b:SetBackdropColor(.045,.090,.104,1);b:SetBackdropBorderColor(C.teal[1],C.teal[2],C.teal[3],1);b.label:SetTextColor(C.teal[1],C.teal[2],C.teal[3],1)
        else b:SetBackdropColor(C.panel2[1],C.panel2[2],C.panel2[3],1);b:SetBackdropBorderColor(C.border[1],C.border[2],C.border[3],C.border[4]);b.label:SetTextColor(C.pale[1],C.pale[2],C.pale[3],1)end
    end
    P.frequencyHint:SetText((RPAL.GetPersonalityFrequencyDescription and RPAL.GetPersonalityFrequencyDescription(freq))or"")
    local living=RPAL.GetPersonaLivingConfig and RPAL.GetPersonaLivingConfig()or{}
    if P.voiceSummary then
        local voiceDef=RPAL.PERSONA_VOICE_PROFILES and RPAL.PERSONA_VOICE_PROFILES[living.voice or"casual"]
        local profDef=RPAL.PERSONA_PROFANITY_LEVELS and RPAL.PERSONA_PROFANITY_LEVELS[living.profanity or"none"]
        local idleCount=0;for _,on in pairs(living.idle and living.idle.selected or{})do if on then idleCount=idleCount+1 end end
        local eff,learned=living.voice,nil
        if RPAL.GetPersonaEffectiveVoice then eff,learned=RPAL.GetPersonaEffectiveVoice() end
        local effectiveProf=living.profanity or"none"
        if RPAL.GetPersonaEffectiveProfanity then effectiveProf=RPAL.GetPersonaEffectiveProfanity() end
        local profEffectiveDef=RPAL.PERSONA_PROFANITY_LEVELS and RPAL.PERSONA_PROFANITY_LEVELS[effectiveProf]
        local profText=string.upper(tostring(profDef and profDef.label or living.profanity or"None"))
        if effectiveProf~=(living.profanity or"none") then profText=profText.."→"..string.upper(tostring(profEffectiveDef and profEffectiveDef.label or effectiveProf)) end
        local adaptive=(living.voiceLearning and living.voiceLearning.enabled) and ("  •  ADAPTIVE "..string.upper(tostring(eff or living.voice or"casual")).." / "..tostring(learned and learned.lines or 0).." LINES") or ""
        P.voiceSummary:SetText(string.upper(tostring(voiceDef and voiceDef.label or living.voice or"Casual")).."  •  PROFANITY "..profText..adaptive.."  •  "..tostring(idleCount).." IDLE HABITS")
    end
    local st=RPAL.GetPersonalityStateSummary and RPAL.GetPersonalityStateSummary()or{};P.needs:SetText(string.format("INTERNAL STATE\nAppetite %d   •   Restless %d\nAlert %d   •   Connection %d   •   Reflection %d",st.appetite or 0,st.restlessness or 0,st.vigilance or 0,st.connection or 0,st.reflection or 0))
    local settings=RPAL.GetSettings and RPAL.GetSettings()or{};if settings.survivalEnabled and RPAL.GetSurvivalState then local ok,s=pcall(RPAL.GetSurvivalState);if ok and s then P.survival:SetText(string.format("SURVIVAL LINK ACTIVE\nFood %d%%   •   Water %d%%\nLow values directly bias food/drink thoughts.",math.floor(tonumber(s.food)or 100),math.floor(tonumber(s.water)or 100)))else P.survival:SetText("SURVIVAL LINK ACTIVE")end else P.survival:SetText("SURVIVAL LINK OFF\nPersona uses its internal needs only.")end
end

local function updateContinuity()
    if not P.continuity then return end
    local st=RPAL.GetPersonaLivingState and RPAL.GetPersonaLivingState()or{}
    P.stateLabel:SetText(tostring(st.label or"CALM"));local stateDetail=tostring(st.detail or"Nothing is pulling especially hard at the moment.")
    if RPAL.GetPersonaActiveConcerns then local concerns=RPAL.GetPersonaActiveConcerns(1);local top=concerns and concerns[1];if top and (tonumber(top.salience)or 0)>=20 then local who=top.person and top.person.name;local label=tostring(top.type or""):gsub("_"," "):lower();stateDetail=stateDetail.."\n\nCurrent pull: "..(who and (who.." — ")or"")..label.." ("..tostring(math.floor((tonumber(top.salience)or 0)+.5)).."/100)." end end;P.stateDetail:SetText(stateDetail)
    local dyn=RPAL.GetPersonaRelationshipDynamics and RPAL.GetPersonaRelationshipDynamics(3,true)or{}
    if#dyn==0 then P.dynamicText:SetText("Nobody nearby is pulling strongly on the Persona.")else
        local lines={};for _,d in ipairs(dyn)do local tone=(d.tension or 0)>=(d.warmth or 0)and((d.tension or 0)>=.42 and"tense"or"watchful")or((d.warmth or 0)>=.42 and"warm"or"familiar");lines[#lines+1]=tostring(d.name or"Someone").." — "..string.upper(tone)end;P.dynamicText:SetText(table.concat(lines,"\n"))
    end
    local refs=RPAL.GetPersonaSessionReflections and RPAL.GetPersonaSessionReflections(1)or{};P.reflectionText:SetText(refs[1]and tostring(refs[1].text or"")or"No Session Reflection yet.")
    local allMem=RPAL.GetPersonaMemories and RPAL.GetPersonaMemories(24)or{};local mem={}
    for _,m in ipairs(allMem)do if RPAL.GetPersonaEpisodeFromMemory and RPAL.GetPersonaEpisodeFromMemory(m)then mem[#mem+1]=m;if#mem>=6 then break end end end
    P.noMemories:SetShown(#mem==0)
    for i=1,6 do local r=P.memoryRows[i];local m=mem[i];r:SetShown(m~=nil);if m then local episode=RPAL.GetPersonaEpisodeFromMemory and RPAL.GetPersonaEpisodeFromMemory(m);r.kind:SetText(string.upper(tostring(episode and episode.status or"episode")));r.text:SetText(tostring(m.text or""))end end

    local insights=RPAL.GetPersonaCharacterInsights and RPAL.GetPersonaCharacterInsights(4)or{};P.noInsights:SetShown(#insights==0)
    for i=1,4 do local r=P.insightRows[i];local insight=insights[i];r:SetShown(insight~=nil);if insight then r.text:SetText(tostring(insight.text or""));r.meta:SetText(tostring(insight.count or 0).." EVIDENCE  •  "..tostring(math.floor((tonumber(insight.confidence)or 0)*100+.5)).."%")end end

    local turning=RPAL.GetPersonaTurningPoints and RPAL.GetPersonaTurningPoints(5)or{};P.noTurning:SetShown(#turning==0)
    for i=1,5 do local r=P.turningRows[i];local row=turning[i];r:SetShown(row~=nil);if row then r.text:SetText(tostring(row.text or""));r.meta:SetText(string.upper(tostring(row.status or"recorded")))end end

    local threads=RPAL.GetPersonaUnresolvedThreads and RPAL.GetPersonaUnresolvedThreads(5)or{};P.noThreads:SetShown(#threads==0)
    for i=1,5 do local r=P.threadRows[i];local row=threads[i];r:SetShown(row~=nil);if row then r.text:SetText(tostring(row.text or""));local residual=tostring(row.residual or"unresolved");r.meta:SetText(string.upper(residual~=""and residual or"UNRESOLVED"))end end

    local check=RPAL.GetPersonaInterpretationCheck and RPAL.GetPersonaInterpretationCheck()or nil;P.currentInterpretationCheck=check
    if check then
        local reaction=tostring(check.reaction or"uncertain"):gsub("%-"," ")
        P.interpretationQuestion:SetText(tostring(check.anchor or"This episode").."\nYour character's response looked "..reaction..". What was underneath it?")
    else P.interpretationQuestion:SetText("No ambiguous major episode needs your input.") end
    for _,b in ipairs(P.interpretationButtons or{})do b:SetShown(check~=nil) end
end

function RPAL.RefreshPersonalityUI()
    build();local d=RPAL.GetPersonalityData and RPAL.GetPersonalityData();if not d or not P.frame then return end
    P.signature:SetText((RPAL.GetPersonalityArchetype and RPAL.GetPersonalityArchetype()or"Unformed").."   •   "..(RPAL.GetPersonalityAlignmentLabel and RPAL.GetPersonalityAlignmentLabel()or"Neutral"))
    updateCore(d);updateLearning(d);updateTraits(d);updateAssociations();updatePeople();updateChanges();updateVoice(d);updateContinuity()
end

local function buildIntro()
    if introFrame then return introFrame end
    local f=CreateFrame("Frame","ODYN_PersonaIntro",UIParent,"BackdropTemplate");introFrame=f;f:SetSize(720,430);f:SetPoint("CENTER");f:SetFrameStrata("FULLSCREEN_DIALOG");f:SetFrameLevel(7600);f:SetToplevel(true);f:SetClampedToScreen(true);f:EnableMouse(true);backdrop(f,C.bg,C.gold);f:Hide()
    local title=font(f,"PERSONA BETA",26,C.gold);title:SetPoint("TOP",0,-42)
    local sub=font(f,"A living inner model for one character.",13,C.teal);sub:SetPoint("TOP",0,-83)
    local body=font(f,"Persona Beta watches only your character's IC roleplay and uses calibration as its starting point. While you remain IC, it can surface natural inner thoughts and small idle impulses even when nobody is speaking to you. Over time it can form Living States, remember important impressions, react differently around familiar people, and carry reflections from earlier roleplay into later moments.\n\nPersona Beta never performs actions for you and it does not replace your writing. It is optional, character-specific, still experimental, and calibration is required before any Persona content becomes visible or active.",12,C.pale);body:SetPoint("TOPLEFT",70,-128);body:SetPoint("TOPRIGHT",-70,-128);body:SetHeight(170);body:SetWordWrap(true);body:SetJustifyH("CENTER");body:SetJustifyV("TOP")
    local enable=button(f,"ENABLE PERSONA BETA",210,40);enable:SetPoint("BOTTOM",0,54);enable.label:SetFont(FONT,12,"");enable:SetScript("OnClick",function()
        if RPAL.EnablePersonaForCharacter then RPAL.EnablePersonaForCharacter()end;f:Hide()
        if C_Timer and C_Timer.After then C_Timer.After(.08,function()if RPAL.OpenPersonalityCalibration then RPAL.OpenPersonalityCalibration()end end)elseif RPAL.OpenPersonalityCalibration then RPAL.OpenPersonalityCalibration()end
    end)
    local close=button(f,"NOT NOW",100,28);close:SetPoint("BOTTOM",0,19);close:SetScript("OnClick",function()f:Hide()end)
    if UISpecialFrames then local found=false;for _,n in ipairs(UISpecialFrames)do if n=="ODYN_PersonaIntro"then found=true break end end;if not found then table.insert(UISpecialFrames,"ODYN_PersonaIntro")end end
    return f
end

function RPAL.OpenPersonalityUI()
    build();if not P.frame then return false end
    if RPAL.IsPersonaOptedIn and not RPAL.IsPersonaOptedIn()then
        P.frame:Hide();local intro=buildIntro();intro:Show();intro:Raise();return true
    end
    if RPAL.IsPersonalityCalibrated and not RPAL.IsPersonalityCalibrated()then
        P.frame:Hide();if introFrame then introFrame:Hide()end;if RPAL.OpenPersonalityCalibration then return RPAL.OpenPersonalityCalibration()end;return false
    end
    if introFrame then introFrame:Hide()end
    local main=(RPAL.UI and RPAL.UI.main)or _G.RPAL_MainFrame;if main and main.GetScale then P.frame:SetScale(main:GetScale()or 1)end
    P.frame:ClearAllPoints();P.frame:SetPoint("CENTER",main or UIParent,"CENTER",0,0);P.frame:Show();P.frame:Raise();RPAL.RefreshPersonalityUI();return true
end
function RPAL.ClosePersonalityUI(silent)
    if P.frame then P.frame:Hide()end
    if introFrame then introFrame:Hide()end
    if not silent and RPAL.RefreshBookChrome then RPAL.RefreshBookChrome(true)end
    return true
end
function RPAL.IsPersonalityOpen()return P.frame and P.frame:IsShown()or false end
