local ADDON_NAME, RPAL = ...

-- ============================================================
-- ODYN Persona Continuity Layer v1
-- Living State + Character Memory + Relationship Dynamics +
-- Session Reflection. This layer never acts for the player; it only gives the
-- calibrated character a persistent internal context while IC.
-- ============================================================

local VERSION=6
local MAX_MEMORIES=72
local MAX_REFLECTIONS=24
local MAX_DYNAMICS=48

local function trim(s)return tostring(s or""):gsub("^%s+",""):gsub("%s+$","")end
local function clamp(v,a,b)v=tonumber(v)or a;if v<a then return a elseif v>b then return b end;return v end
local function now()return time and time() or 0 end
local function shortName(v)return trim(v):match("^[^-]+")or trim(v)end
local function normName(v)return shortName(v):lower():gsub("[^%w%s']",""):gsub("%s+"," ")end
local function copyAlignment(a)return{good=tonumber(a and a.good)or .34,neutral=tonumber(a and a.neutral)or .46,evil=tonumber(a and a.evil)or .20}end

local function ready(requireIC)
    if RPAL.IsPersonaSystemEnabled and not RPAL.IsPersonaSystemEnabled()then return false end
    if RPAL.IsPersonalityCalibrated and not RPAL.IsPersonalityCalibrated()then return false end
    if requireIC~=false and RPAL.IsTRPInCharacter then
        local ok,res=pcall(RPAL.IsTRPInCharacter);if not ok or not res then return false end
    end
    return true
end

local function retireGenericSceneMemories(c)
    if type(c)~="table"or(tonumber(c.version)or 0)>=4 or c.concreteMemoryV4MigratedAt then return 0 end
    c.memories=type(c.memories)=="table"and c.memories or{}
    c.reflections=type(c.reflections)=="table"and c.reflections or{}
    local kept,removedTexts,removed={}, {},0
    for _,memory in ipairs(c.memories)do
        if memory.kind=="scene"or memory.kind=="session"then
            removed=removed+1;removedTexts[trim(memory.text):lower()]=true
        else kept[#kept+1]=memory end
    end
    c.memories=kept
    if removed>0 then
        for _,reflection in ipairs(c.reflections)do
            local rows={}
            for _,text in ipairs(type(reflection.memoriesFormed)=="table"and reflection.memoriesFormed or{})do
                if not removedTexts[trim(text):lower()]then rows[#rows+1]=text end
            end
            reflection.memoriesFormed=rows
        end
    end
    c.concreteMemoryV4MigratedAt=now();c.concreteMemoryV4Removed=removed;c.version=VERSION
    return removed
end

local function repairEpisodeMemoryStore(c)
    if type(c)~="table"or c.episodeMemoryV5RepairedAt then return 0 end
    c.memories=type(c.memories)=="table"and c.memories or{}
    local out,byEvent,removed={}, {},0
    for _,memory in ipairs(c.memories)do
        local meta=type(memory.meta)=="table"and memory.meta or{};local episode=type(meta.episode)=="table"and meta.episode or nil
        local eventID=meta.eventID or(episode and episode.eventID)
        if memory.kind=="episode"and eventID~=nil then
            local key=tostring(eventID);local sourceAt=tonumber(episode and(episode.at or(episode.observation and episode.observation.at)))or tonumber(memory.at)or now()
            memory.at=sourceAt;memory.lastAt=sourceAt;memory.signature=("episode|event:"..key):sub(1,220)
            local old=byEvent[key]
            if old then
                removed=removed+1
                local oldEp=type(old.meta)=="table"and old.meta.episode or nil
                if(tonumber(episode and episode.version)or 0)>(tonumber(oldEp and oldEp.version)or 0)then
                    for k,v in pairs(memory)do old[k]=v end
                end
            else byEvent[key]=memory;out[#out+1]=memory end
        else out[#out+1]=memory end
    end
    c.memories=out;c.episodeMemoryV5RepairedAt=now();c.episodeMemoryV5DuplicatesRemoved=removed
    return removed
end

function RPAL.MigratePersonaConcreteMemoryStore(personality)
    if type(personality)~="table"then return 0 end
    personality.continuity=type(personality.continuity)=="table"and personality.continuity or{}
    local n=retireGenericSceneMemories(personality.continuity)
    return n+repairEpisodeMemoryStore(personality.continuity)
end

local function ensure()
    local p=RPAL.GetPersonalityData and RPAL.GetPersonalityData();if not p then return nil,nil end
    p.continuity=type(p.continuity)=="table"and p.continuity or{}
    local c=p.continuity
    c.memories=type(c.memories)=="table"and c.memories or{}
    c.reflections=type(c.reflections)=="table"and c.reflections or{}
    c.dynamics=type(c.dynamics)=="table"and c.dynamics or{}
    c.living=type(c.living)=="table"and c.living or{label="CALM",detail="Nothing is pulling especially hard at the moment.",score=.35,since=now(),updatedAt=0}
    c.session=type(c.session)=="table"and c.session or{}
    c.processed=type(c.processed)=="table"and c.processed or{}
    retireGenericSceneMemories(c)
    repairEpisodeMemoryStore(c)
    if not c.relationshipEvidenceV6MigratedAt then
        -- Pre-v6 mixed declared TRP priors directly into observed warmth/tension.
        -- Those values cannot be separated reliably after the fact, so retire
        -- the mixed surface while preserving actual familiarity/interactions and
        -- the richer cognitive dimensions. Future warmth/tension are observed RP
        -- only; declared priors live in separate fields.
        for _,d in pairs(c.dynamics)do
            if type(d)=="table"then
                d.observedWarmth=0;d.observedTension=0;d.warmth=0;d.tension=0
                d.priorWarmth=0;d.priorTension=0;d.priorAttraction=0
                d.declaredScore=0;d.declaredConfidence=0;d.readScore=0;d.readConfidence=0
            end
        end
        -- Relationship deltas generated before this split could describe a
        -- metadata seed as a session change. Keep the reflection text/history,
        -- but retire those unreliable numeric deltas and generic seed memories.
        for _,r in ipairs(c.reflections)do if type(r)=="table"then r.relationshipChanges={};r.relationshipDeltasRetiredV6=true end end
        local kept={}
        for _,m in ipairs(c.memories)do
            local meta=type(m.meta)=="table"and m.meta or{}
            local legacySeedMemory=m.kind=="relationship"and meta.eventID==nil and meta.episode==nil
            if not legacySeedMemory then kept[#kept+1]=m end
        end
        c.memories=kept;c.relationshipEvidenceV6MigratedAt=now()
    end
    c.version=VERSION
    return c,p
end

local function pushFront(t,row,max)
    table.insert(t,1,row);while#t>(max or 50)do table.remove(t)end
end

function RPAL.AddPersonaMemory(kind,text,weight,meta)
    local c=ensure();text=trim(text);if not c or text==""then return false end
    kind=tostring(kind or"moment");weight=clamp(weight or .5,.05,1);meta=type(meta)=="table"and meta or{}
    local eventID=meta.eventID;local episode=type(meta.episode)=="table"and meta.episode or nil
    local sourceAt=kind=="episode"and(tonumber(episode and(episode.at or(episode.observation and episode.observation.at)))or tonumber(meta.at))or nil
    local sig
    if kind=="episode"and eventID~=nil then sig=("episode|event:"..tostring(eventID)):sub(1,220)
    else sig=(kind.."|"..text:lower():gsub("%s+"," ")):sub(1,220)end
    local t=now()
    for _,m in ipairs(c.memories)do
        local mm=type(m.meta)=="table"and m.meta or{}
        local sameEvent=kind=="episode"and eventID~=nil and tostring(mm.eventID or(type(mm.episode)=="table"and mm.episode.eventID)or"")==tostring(eventID)
        if m.signature==sig or sameEvent then
            m.signature=sig;m.weight=math.max(tonumber(m.weight)or .4,weight);m.count=math.max(1,tonumber(m.count)or 1)
            if kind=="episode"then
                local at=sourceAt or tonumber(m.at)or t;m.at=at;m.lastAt=at
            elseif t-(tonumber(m.lastAt)or tonumber(m.at)or 0)<7*86400 then
                m.weight=clamp((tonumber(m.weight)or .4)+weight*.18,.05,1);m.lastAt=t;m.count=(tonumber(m.count)or 1)+1
            end
            m.meta=meta;return m
        end
    end
    local at=sourceAt or t
    local row={kind=kind,text=text,weight=weight,at=at,lastAt=at,count=1,signature=sig,meta=meta}
    pushFront(c.memories,row,MAX_MEMORIES)
    return row
end

function RPAL.GetPersonaMemories(limit)
    local c=ensure();local out={};if not c then return out end
    limit=math.max(1,math.min(MAX_MEMORIES,math.floor(tonumber(limit)or 8)))
    for i=1,math.min(limit,#c.memories)do out[#out+1]=c.memories[i]end
    return out
end

function RPAL.GetPersonaSessionReflections(limit)
    local c=ensure();local out={};if not c then return out end
    limit=math.max(1,math.min(MAX_REFLECTIONS,math.floor(tonumber(limit)or 5)))
    for i=1,math.min(limit,#c.reflections)do out[#out+1]=c.reflections[i]end
    return out
end

local function normUnit(v)return tostring(v or""):lower():gsub("%s",""):gsub("'","")end
local function unitShort(v)return normUnit(v):match("^[^-]+")or normUnit(v)end
local PERSONA_IDENTITY_CACHE_TTL=8
local personaIdentityIndex
local personaIdentityIndexAt=-100
local function identityRuntimeNow()return GetTime and GetTime()or 0 end
local function getPersonaIdentityIndex()
    local t=identityRuntimeNow()
    if personaIdentityIndex and t-personaIdentityIndexAt<PERSONA_IDENTITY_CACHE_TTL then return personaIdentityIndex end
    local index={unit={},name={}}
    local function first(map,key,row)
        if key and key~=""and map[key]==nil then map[key]=row end
    end
    for _,r in ipairs((RPAL.GetPersonaPeople and RPAL.GetPersonaPeople(20))or{})do
        local display=trim(r.display);local full=trim(r.trpFullName)
        first(index.unit,normUnit(r.key),r)
        first(index.unit,unitShort(r.key),r)
        first(index.name,normName(display),r)
        first(index.name,normName(full),r)
    end
    personaIdentityIndex=index;personaIdentityIndexAt=t
    return index
end
local function personaIdentity(name)
    local wantedUnit=normUnit(name);local wantedShort=unitShort(name);local wantedName=normName(name)
    local index=getPersonaIdentityIndex()
    local r=index.unit[wantedUnit]or index.unit[wantedShort]or index.name[wantedName]
    local display=r and trim(r.display)or""
    if display~=""then return display,r end
    return nil
end

local function personRead(name)
    local _,row=personaIdentity(name);return row
end

local function ensureDynamic(name,knownRow)
    local c=ensure();if not c then return nil end
    local display,row=name,knownRow
    if not row then display,row=personaIdentity(name)end
    if not display then return nil end
    local key=normName(display);if key==""then return nil end
    local d=c.dynamics[key]
    if type(d)~="table"then d={key=key,name=display,warmth=0,tension=0,familiarity=0,interactions=0,lastAt=0,presentUntil=0};c.dynamics[key]=d end
    d.name=display;d.sourceKey=row and row.key or d.sourceKey;d.trpFullName=row and row.trpFullName or d.trpFullName
    return d,row
end

local function syncDynamicRead(d,knownRow)
    local r=knownRow or personRead(d.name or d.sourceKey or d.key)
    if r then
        d.name=trim(r.display)~=""and trim(r.display)or d.name
        local verified=r.trpSemanticVerified==true
        local declared=verified and clamp(tonumber(r.trpSeedScore)or 0,-1,1)or 0
        local conf=verified and .34 or 0
        d.declaredScore=declared;d.declaredConfidence=conf;d.declaredKind=tostring(r.trpRelation or(verified and r.state)or"familiar")
        d.priorWarmth=math.max(0,declared)*.28;d.priorTension=math.max(0,-declared)*.28
        d.priorFamiliarity=math.max(tonumber(r.familiarityPrior)or 0,(r.trpFamiliarityOnly or r.trpRelation)and .12 or 0)
        d.priorAttraction=(verified and d.declaredKind=="love")and .12 or 0
        d.observedWarmth=clamp(tonumber(d.observedWarmth)or tonumber(d.warmth)or 0,0,1)
        d.observedTension=clamp(tonumber(d.observedTension)or tonumber(d.tension)or 0,0,1)
        d.warmth=d.observedWarmth;d.tension=d.observedTension
        local observedScore=d.observedWarmth-d.observedTension
        d.readScore=clamp(observedScore+declared*.35,-1,1);d.readConfidence=clamp(math.max(tonumber(r.confidence)or 0,conf),0,1)
    end
end

local function touchDynamic(name,eventName)
    local display,row=personaIdentity(name);if not display then return end
    local d=ensureDynamic(display,row);if not d then return end
    syncDynamicRead(d,row)
    d.interactions=(tonumber(d.interactions)or 0)+1
    d.familiarity=clamp((tonumber(d.familiarity)or 0)+((eventName=="CHAT_MSG_EMOTE")and .022 or .016),0,1)
    d.lastAt=now();d.presentUntil=d.lastAt+8*60
end

function RPAL.GetPersonaRelationshipDynamics(limit,onlyPresent)
    local c=ensure();local out={};if not c then return out end;local t=now()
    for _,d in pairs(c.dynamics)do
        if type(d)=="table"then
            local display,row=personaIdentity(d.sourceKey or d.trpFullName or d.name or d.key)
            if display then
                d.name=display;syncDynamicRead(d,row)
                local present=(tonumber(d.presentUntil)or 0)>=t
                if(not onlyPresent)or present then
                    local ow=tonumber(d.observedWarmth)or tonumber(d.warmth)or 0;local ot=tonumber(d.observedTension)or tonumber(d.tension)or 0;local of=tonumber(d.familiarity)or 0
                    out[#out+1]={key=d.key,name=display,warmth=clamp(ow+(tonumber(d.priorWarmth)or 0),0,1),tension=clamp(ot+(tonumber(d.priorTension)or 0),0,1),familiarity=math.max(of,tonumber(d.priorFamiliarity)or 0),observedWarmth=ow,observedTension=ot,observedFamiliarity=of,priorWarmth=tonumber(d.priorWarmth)or 0,priorTension=tonumber(d.priorTension)or 0,priorFamiliarity=tonumber(d.priorFamiliarity)or 0,declaredScore=tonumber(d.declaredScore)or 0,declaredKind=d.declaredKind,interactions=tonumber(d.interactions)or 0,lastAt=tonumber(d.lastAt)or 0,present=present,score=tonumber(d.readScore)or 0,confidence=tonumber(d.readConfidence)or 0,recentInteractions=row and row.recentInteractions or 0,recentSessions=row and row.recentSessions or 0}
                end
            end
        end
    end
    table.sort(out,function(a,b)
        if a.present~=b.present then return a.present end
        local av=(a.warmth+a.tension)*.7+a.familiarity*.3;local bv=(b.warmth+b.tension)*.7+b.familiarity*.3
        if av==bv then return a.lastAt>b.lastAt end;return av>bv
    end)
    limit=math.max(1,math.min(MAX_DYNAMICS,math.floor(tonumber(limit)or 8)));while#out>limit do table.remove(out)end
    return out
end

local function injuryPressure()
    local db=RPAL.GetHealth2DB and RPAL.GetHealth2DB();if not db then return 0,nil end
    local rank={minor=.18,moderate=.40,severe=.72,critical=1};local best=0;local label=nil
    for _,item in pairs(type(db.items)=="table"and db.items or{})do
        if type(item)=="table"and item.category=="injury"and tostring(item.stage or""):lower()~="healed"then
            local r=rank[tostring(item.severity or"moderate"):lower()]or .4
            if r>best then best=r;label=trim(item.customName or item.type or"injury")end
        end
    end
    return best,label
end

local STATE_TEXT={
    CALM="Nothing is pulling especially hard at the moment.",
    RESTLESS="Stillness is starting to feel uncomfortable.",
    GUARDED="Attention keeps returning to what might go wrong.",
    WARM="Other people feel a little easier to be around right now.",
    IRRITATED="Something under the surface keeps catching and scraping.",
    REFLECTIVE="Old moments and unfinished thoughts keep resurfacing.",
    FOCUSED="Attention has narrowed around what is immediately in front of them.",
    WEARY="There is a low, persistent drag behind everything else.",
    UNCOMFORTABLE="Physical discomfort keeps intruding on everything else.",
    HURT="A recent emotional wound is still taking up more space than ordinary thoughts.",
    DISTRAUGHT="Strong emotion is crowding out concentration and ordinary background concerns.",
    SELF_CONSCIOUS="A recent social moment is still being replayed with uncomfortable attention.",
    WITHDRAWN="The character is pulling inward while a recent hurt continues to settle.",
    RELIEVED="A recent source of pressure has eased enough to be felt physically and mentally.",
    TENSE="Attention is staying close to possible trouble after a recent threat or conflict.",
    CONCERNED="Someone important seems distressed enough that attention keeps drifting back toward them.",
}

local function refreshLivingStateBadge()
    if C_Timer and C_Timer.After then
        C_Timer.After(0,function()
            if RPAL.RefreshCharacterPreview then pcall(RPAL.RefreshCharacterPreview) end
        end)
    elseif RPAL.RefreshCharacterPreview then pcall(RPAL.RefreshCharacterPreview) end
end

local function computeLivingState(force)
    local c,p=ensure();if not c or not p then return nil end
    local t=now();if not force and t-(tonumber(c.living.updatedAt)or 0)<45 then return c.living end
    local s=p.state or{};local present=RPAL.GetPersonaRelationshipDynamics and RPAL.GetPersonaRelationshipDynamics(8,true)or{}
    local warm,tension=0,0
    for _,d in ipairs(present)do warm=math.max(warm,d.warmth or 0);tension=math.max(tension,d.tension or 0)end
    local injury,injuryName=injuryPressure()
    local scores={
        CALM=.38+(1-(tonumber(s.restlessness)or .3))*.15+(1-(tonumber(s.vigilance)or .35))*.10,
        RESTLESS=(tonumber(s.restlessness)or .3)*.92,
        GUARDED=(tonumber(s.vigilance)or .35)*.78+tension*.48,
        WARM=(tonumber(s.connection)or .45)*.50+warm*.60,
        IRRITATED=tension*.82+(tonumber(s.restlessness)or .3)*.20,
        REFLECTIVE=(tonumber(s.reflection)or .45)*.82,
        FOCUSED=.36+(tonumber(s.vigilance)or .35)*.18+(1-(tonumber(s.restlessness)or .3))*.16,
        WEARY=(tonumber(s.appetite)or .34)*.28+injury*.20,
        UNCOMFORTABLE=injury*.95,
    }
    local label,best="CALM",-1
    for k,v in pairs(scores)do if v>best then label,best=k,v end end
    local old=tostring(c.living.label or"CALM")
    if not force and old~=label and scores[old]and(scores[old]+.07)>=best then label=old;best=scores[old]end
    if old~=label then c.living.since=t end
    c.living.label=label;c.living.score=clamp(best,0,1);c.living.updatedAt=t;c.living.detail=STATE_TEXT[label]or STATE_TEXT.CALM
    if label=="UNCOMFORTABLE"and injuryName and injuryName~=""then c.living.detail="The "..injuryName:lower().." keeps intruding on everything else."end
    if old~=label then refreshLivingStateBadge() end
    return c.living
end

function RPAL.GetPersonaLivingState()
    local s=computeLivingState(false);if not s then return{}end
    return{label=s.label,detail=s.detail,score=s.score,since=s.since,updatedAt=s.updatedAt}
end

function RPAL.NudgePersonaLivingState(kind,amount)
    local c,p=ensure();if not p then return false end;amount=clamp(amount or .05,-.3,.3);local st=p.state or{}
    if kind=="social"then st.connection=clamp((st.connection or .45)-amount,0,1)
    elseif kind=="tension"then st.vigilance=clamp((st.vigilance or .35)+amount,0,1);st.restlessness=clamp((st.restlessness or .3)+amount*.45,0,1)
    elseif kind=="restless"then st.restlessness=clamp((st.restlessness or .3)+amount,0,1)
    elseif kind=="reflective"then st.reflection=clamp((st.reflection or .45)+amount,0,1)
    elseif kind=="settled"then st.restlessness=clamp((st.restlessness or .3)-math.abs(amount),0,1)end
    if c and c.living then c.living.updatedAt=0 end;computeLivingState(true);return true
end

local function dominantAlignmentDelta(before,after)
    before=before or{};after=after or{}
    local vals={{"Good",(after.good or 0)-(before.good or 0)},{"Neutral",(after.neutral or 0)-(before.neutral or 0)},{"Evil",(after.evil or 0)-(before.evil or 0)}}
    table.sort(vals,function(a,b)return a[2]>b[2]end)
    if(vals[1][2]or 0)>=.0025 then return vals[1][1],vals[1][2]end
end

local function stateSnapshot(force)
    local st=computeLivingState(force)or{}
    return{label=tostring(st.label or"CALM"),detail=tostring(st.detail or STATE_TEXT.CALM),score=tonumber(st.score)or 0}
end

local function onSceneStarted(scene)
    if not ready(false)then return end
    local c=ensure();if not c then return end
    local a=(RPAL.GetEffectivePersonalityAlignment and RPAL.GetEffectivePersonalityAlignment())or{good=.34,neutral=.46,evil=.20}
    c.session={id=scene and scene.id or tostring(now()),startedAt=now(),alignment=copyAlignment(a),state=stateSnapshot(true),dynamics={}}
    for _,d in ipairs(RPAL.GetPersonaRelationshipDynamics(20,false)or{})do c.session.dynamics[d.key]={name=d.name,warmth=d.warmth,tension=d.tension,familiarity=d.familiarity,observedWarmth=d.observedWarmth or 0,observedTension=d.observedTension or 0,observedFamiliarity=d.observedFamiliarity or 0,declaredScore=d.declaredScore or 0,score=d.score}end
end

local function relationshipSessionChanges(start,names)
    local rows={}
    for _,name in ipairs(names or{})do
        local key=normName(name);local before=type(start and start.dynamics)=="table"and start.dynamics[key]or nil
        local d,row=ensureDynamic(name);if d then syncDynamicRead(d,row)end
        local ow=tonumber(d and d.observedWarmth)or 0;local ot=tonumber(d and d.observedTension)or 0;local of=tonumber(d and d.familiarity)or 0
        local after={warmth=clamp(ow+(tonumber(d and d.priorWarmth)or 0),0,1),tension=clamp(ot+(tonumber(d and d.priorTension)or 0),0,1),familiarity=math.max(of,tonumber(d and d.priorFamiliarity)or 0),observedWarmth=ow,observedTension=ot,observedFamiliarity=of,score=tonumber(d and d.readScore)or 0}
        local bw=tonumber(before and before.observedWarmth)or 0;local bt=tonumber(before and before.observedTension)or 0;local bf=tonumber(before and before.observedFamiliarity)or 0
        local dw=after.observedWarmth-bw;local dt=after.observedTension-bt;local df=after.observedFamiliarity-bf
        rows[#rows+1]={name=shortName(name),warmth=after.warmth,tension=after.tension,familiarity=after.familiarity,score=after.score,observedWarmth=after.observedWarmth,observedTension=after.observedTension,observedFamiliarity=after.observedFamiliarity,deltaWarmth=dw,deltaTension=dt,deltaFamiliarity=df,impact=math.max(math.abs(dw),math.abs(dt),math.abs(df))}
    end
    table.sort(rows,function(a,b)if a.impact==b.impact then return(a.familiarity or 0)>(b.familiarity or 0)end;return a.impact>b.impact end)
    return rows
end

local function onSceneEnded(scene)
    if not ready(false)then return end
    local c,p=ensure();if not c then return end
    local cognition=p and p.continuity and p.continuity.cognition
    if RPAL.FinalizePersonaEpisodes and type(cognition)=="table"then pcall(RPAL.FinalizePersonaEpisodes,cognition,p,true)end
    local start=c.session or{};local after=(RPAL.GetEffectivePersonalityAlignment and RPAL.GetEffectivePersonalityAlignment())or{good=.34,neutral=.46,evil=.20}
    local names={};local seenNames={}
    local function addIdentity(v)
        local display=personaIdentity(v);if display then local k=normName(display);if k~=""and not seenNames[k]then seenNames[k]=true;names[#names+1]=display end end
    end
    local owned=RPAL.GetPersonaOwnedConversationPeople and RPAL.GetPersonaOwnedConversationPeople(16,scene and scene.id)or{}
    for _,person in ipairs(owned)do addIdentity(person.name)end
    if#names==0 and type(scene and scene.participants)=="table"then
        for _,person in pairs(scene.participants)do
            if(tonumber(person.interactions)or 0)>0 then addIdentity(person.unitName or person.trpName or person.name)end
        end
    end
    local toward,delta=dominantAlignmentDelta(start.alignment,after)
    local finalState=stateSnapshot(true);local initialState=type(start.state)=="table"and start.state or{label=tostring(start.state or"CALM"),detail=""}
    local relationshipChanges=relationshipSessionChanges(start,names)
    local pieces={}
    if#names>0 then
        local shown={};for i=1,math.min(3,#names)do shown[#shown+1]=names[i]end
        pieces[#pieces+1]="The time with "..table.concat(shown,", ")..(#names>3 and(" and "..tostring(#names-3).." others")or"").." left a few things lingering."
    else
        pieces[#pieces+1]="It was quiet, but a few things still stuck with me."
    end
    if initialState.label~=finalState.label then pieces[#pieces+1]="The character moved from "..tostring(initialState.label):lower().." to "..tostring(finalState.label):lower().." by the end."end
    if toward then pieces[#pieces+1]="The Moral Core shifted slightly toward "..toward.."."end
    if relationshipChanges[1]and(relationshipChanges[1].impact or 0)>=.015 then
        local r=relationshipChanges[1];local feeling=(r.deltaTension or 0)>(r.deltaWarmth or 0)and"more tense"or((r.deltaWarmth or 0)>.01 and"warmer"or"more familiar")
        pieces[#pieces+1]=r.name.." left the strongest social impression, feeling "..feeling.." than before."
    end
    local text=table.concat(pieces," ")
    local row={at=now(),sceneID=scene and scene.id,title=trim(scene and scene.title)~=""and scene.title or"Session reflection",text=text,participants=names,state=finalState.label,stateBefore=initialState,stateAfter=finalState,toward=toward,moralDelta=delta,alignmentBefore=copyAlignment(start.alignment),alignmentAfter=copyAlignment(after),relationshipChanges=relationshipChanges,duration=tonumber(scene and(scene.duration or scene.activeSeconds))or 0,memoriesFormed={}}
    pushFront(c.reflections,row,MAX_REFLECTIONS)
    -- A reflection may stay broad, but Character Memory no longer stores that
    -- broad recap as if it were an event. Pull only concrete episodes formed
    -- during this scene; quiet scenes simply form no durable event memory.
    local startedAt=tonumber(start.startedAt)or tonumber(scene and scene.startedAt)or 0
    if startedAt<=0 then startedAt=now()-math.max(2,tonumber(scene and(scene.duration or scene.activeSeconds))or 0)end
    local endedAt=tonumber(scene and scene.endedAt)or now()
    for _,memory in ipairs(c.memories)do
        local meta=type(memory.meta)=="table"and memory.meta or{};local episode=type(meta.episode)=="table"and meta.episode or{}
        local at=tonumber(episode.at)or tonumber(episode.observation and episode.observation.at)or tonumber(memory.at)or 0
        local sceneMatch=trim(episode.sceneID)~=""and trim(scene and scene.id)~=""and tostring(episode.sceneID)==tostring(scene.id)
        if memory.kind=="episode"and((sceneMatch and at<=endedAt+2)or(at>=math.max(0,startedAt-2)and at<=endedAt+2))then
            row.memoriesFormed[#row.memoriesFormed+1]=tostring(memory.text or"")
            if#row.memoriesFormed>=4 then break end
        end
    end
    for i=1,math.min(3,#relationshipChanges)do
        local r=relationshipChanges[i];local observedScore=(tonumber(r.observedWarmth)or 0)-(tonumber(r.observedTension)or 0)
        -- Declared TRP metadata alone never writes Character Memory. Only a
        -- meaningful observed change during the scene may create this broad
        -- relationship memory, and even then it stays conservative.
        if(r.impact or 0)>=.12 and math.abs(observedScore)>=.20 then
            local mem=RPAL.AddPersonaMemory("relationship",r.name.." has been standing out in my mind lately.",.46+math.min(.20,math.abs(observedScore)*.2),{name=r.name,score=observedScore,observed=true,sceneID=scene and scene.id})
            if mem then row.memoriesFormed[#row.memoriesFormed+1]=tostring(mem.text or(r.name.." has been standing out in my mind lately."))end
        end
    end
    c.session={}
    -- Never compete with the Session Recorded screen. SceneUI opens that first;
    -- when the recap is dismissed, this pending reflection becomes a full panel.
    RPAL._personaPendingSessionReflection=row
    if RPAL.RefreshPersonalityUI and RPAL.IsPersonalityOpen and RPAL.IsPersonalityOpen()then RPAL.RefreshPersonalityUI()end
end

local previousSceneStarted=RPAL.OnSceneStarted
RPAL.OnSceneStarted=function(scene)if previousSceneStarted then pcall(previousSceneStarted,scene)end;onSceneStarted(scene)end
local previousSceneEnded=RPAL.OnSceneEnded
RPAL.OnSceneEnded=function(scene)if previousSceneEnded then pcall(previousSceneEnded,scene)end;onSceneEnded(scene)end

local previousLearningChange=RPAL.OnPersonalityLearningChange
RPAL.OnPersonalityLearningChange=function(row)
    if previousLearningChange then pcall(previousLearningChange,row)end
    if not row or not ready(false)then return end
    if row.kind=="alignment"then
        RPAL.AddPersonaMemory("moral","Something in the character has been pulling toward "..tostring(row.toward or row.label or"a different moral direction")..".",.72,{toward=row.toward})
    elseif row.kind=="relationship"then
        local name=personaIdentity(row.key or row.display);if name then RPAL.AddPersonaMemory("relationship",name.." has been leaving a stronger impression lately.",.64,{name=name,score=tonumber(row.score)or 0})end
    elseif row.kind=="trait"and row.label then
        RPAL.AddPersonaMemory("trait","Lately the character has felt more "..tostring(row.label):lower().." than before.",.48,{label=row.label,key=row.key})
    elseif row.kind=="association"then
        local label=trim(row.display or row.key);if label~=""then local score=tonumber(row.score)or 0;RPAL.AddPersonaMemory("association",label..(score<0 and" has been leaving a worse impression lately."or" has been leaving a better impression lately."),.46,{label=label,score=score})end
    end
end

local SAFE_STATE_THOUGHTS={
    CALM={
        "Quiet for once.",
        "Nothing is pressing at me right now.",
        "I could get used to a moment this still.",
        "Nice when my head isn't pulling in six directions.",
        "Everything feels unusually settled.",
        "No urgency. That's rare enough to notice.",
        "I don't mind the quiet at all right now.",
        "For once, just standing here feels fine.",
        "My thoughts are finally leaving a little room between themselves.",
        "This kind of calm never lasts forever, but it's nice while it's here.",
        "My shoulders feel a little looser than they did earlier.",
        "Breathing feels easy enough that I barely notice it.",
        "Nothing around me feels like it needs an immediate reaction.",
        "I can let the background stay in the background for a while.",
        "There is no particular rush in me at the moment.",
        "My attention can wander without getting snagged on anything.",
        "Even the little noises around me feel distant enough.",
        "I feel content to let a few minutes pass without filling them.",
        "The usual tension is quiet enough to forget for a bit.",
        "I am not waiting for anything to go wrong right now.",
        "My hands have finally stopped looking for something to do.",
        "It feels good not to be mentally braced for anything.",
        "There is a comfortable amount of space in my head right now.",
        "I can hear myself think without every thought crowding the next one.",
        "The pace of everything feels manageable for once.",
        "Nothing is tugging hard enough to pull me out of this mood.",
        "I feel more settled in my own skin than usual.",
        "The quiet is sitting well with me.",
        "I am not in any hurry to be somewhere else.",
        "Even waiting feels easy right now.",
        "My breathing has fallen into a slow, comfortable rhythm.",
        "I can stand here without feeling the need to fill the silence.",
        "There is something pleasant about not having much on my mind.",
        "My thoughts feel less crowded than they usually do.",
        "Nothing in particular is bothering me, and I notice the absence of it.",
        "I feel steady enough that small distractions barely register.",
        "There is no knot in my stomach asking for attention.",
        "I can let things be ordinary for a while.",
        "My head feels clear in a very plain, welcome way.",
        "The world feels a little less demanding at the moment.",
        "I am comfortable enough not to keep checking the time.",
        "My body feels like it has finally unclenched a little.",
        "The silence does not feel empty right now.",
        "I could stay like this for a while without minding.",
        "Nothing feels especially urgent, and that suits me fine.",
        "My thoughts are moving at an easy pace.",
        "I am not reaching for the next thing yet.",
        "There is a simple ease to everything right now.",
    },
    RESTLESS={
        "Can't quite settle.",
        "Too much stillness. It's starting to itch.",
        "My hands feel too idle.",
        "I need something to break the sameness.",
        "Standing around is getting harder than it should be.",
        "There's too much spare energy under my skin.",
        "I keep wanting to move for no particular reason.",
        "This quiet is starting to feel cramped.",
        "I could use something—anything—to happen.",
        "My attention keeps looking for somewhere else to go.",
        "My feet keep wanting to shift even when I stay put.",
        "I keep changing how I stand without meaning to.",
        "My fingers are looking for something to mess with again.",
        "There is an annoying amount of energy with nowhere to go.",
        "I keep glancing around just to have something different to look at.",
        "Staying in one place is starting to feel longer than it actually is.",
        "My thoughts keep jumping tracks before they get anywhere.",
        "I can feel myself getting bored with the same view.",
        "I keep wanting to stretch just to do something.",
        "My attention will not stay parked for long.",
        "Every few seconds I catch myself wanting to move again.",
        "I could do with a change of pace right about now.",
        "My body feels ready to go somewhere even if my head has no destination.",
        "I am starting to notice how long I have been standing still.",
        "The silence is making every small movement feel tempting.",
        "My hands keep finding reasons not to stay still.",
        "I keep looking for a reason to walk somewhere.",
        "I feel like I have been waiting longer than I probably have.",
        "There is a little too much energy buzzing around in me.",
        "I keep shifting my weight before I realize I am doing it.",
        "My mind keeps reaching for the next distraction.",
        "I would not mind having something new to occupy me.",
        "The same few thoughts keep getting interrupted by the urge to move.",
        "I keep checking my surroundings out of sheer restlessness.",
        "My legs feel more awake than the rest of me.",
        "Even a short walk sounds appealing just for the change.",
        "I can feel impatience building without anything specific to blame.",
        "My fingers keep wanting to drum against something.",
        "I am getting tired of hearing my own thoughts loop around.",
        "There is no good reason to feel this fidgety, but here it is.",
        "I keep wondering if there is somewhere else I could be for a bit.",
        "The longer I stay still, the more noticeable it becomes.",
        "My attention keeps slipping toward movement around me.",
        "I am starting to feel boxed in by doing nothing.",
        "I keep catching myself ready to leave before I have chosen anywhere to go.",
        "Stillness is taking more effort than it ought to.",
        "I feel like I could pace a hole into the floor if I let myself.",
        "Something in me keeps asking for a little motion.",
    },
    GUARDED={
        "Something has me watching the edges again.",
        "I keep noticing every little movement around me.",
        "Hard to fully relax right now.",
        "My attention keeps drifting toward exits and blind spots.",
        "Something feels just uncertain enough to keep me alert.",
        "I don't like how often I'm checking the room.",
        "Every little shift keeps catching my eye.",
        "Part of me is waiting for the mood to change.",
        "I can feel myself listening harder than usual.",
        "Not frightened. Just not relaxed either.",
        "I keep taking note of who is close enough to matter.",
        "My eyes keep returning to the same corners of the room.",
        "I am noticing footsteps more than I normally would.",
        "It is difficult to stop tracking movement around me.",
        "I keep checking what is behind me without fully turning.",
        "My shoulders feel a little too ready for something.",
        "I am listening for changes in tone as much as words.",
        "I keep leaving a little more space around myself than usual.",
        "There is no obvious danger, but my attention has not accepted that yet.",
        "I keep noticing where I would move if I had to.",
        "My gaze keeps lingering on hands, doors, and sudden movement.",
        "I am reading faces a little more carefully than usual.",
        "Something about the atmosphere has me keeping my guard up.",
        "I keep catching details that would normally slide past me.",
        "I am not quite comfortable turning my back on the room.",
        "Every abrupt sound gets a little more attention than it deserves.",
        "I can feel myself staying half a step removed from everyone around me.",
        "I keep measuring distances without consciously deciding to.",
        "My attention is spread too evenly across the whole place.",
        "I am noticing who comes and goes more than I usually do.",
        "I keep waiting for the harmless explanation before I relax.",
        "My posture feels more controlled than comfortable right now.",
        "I am keeping more of my thoughts to myself than usual.",
        "I keep looking twice at things that probably only need one glance.",
        "There is an edge of caution sitting under everything else.",
        "I am finding it difficult to take people entirely at face value right now.",
        "My attention keeps returning to anything I cannot immediately account for.",
        "I keep noticing the places I cannot see clearly.",
        "I am a little too aware of how close everyone is standing.",
        "Nothing has happened, but I still feel prepared for something to.",
        "I keep hearing the room as much as I am looking at it.",
        "My eyes are moving before I consciously choose where to look.",
        "I am not tense enough to leave, just tense enough to notice everything.",
        "I keep holding onto small details in case they matter later.",
        "It feels easier to stay observant than to relax completely.",
        "My attention keeps circling back to the same uncertain things.",
        "I am giving every interruption a little more weight than usual.",
        "Some part of me has decided this is not the time to switch off.",
    },
    WARM={
        "Company doesn't sound half bad right now.",
        "People feel easier to be around today.",
        "There is something comforting about familiar voices.",
        "I wouldn't mind a familiar face turning up.",
        "Conversation sounds better than silence right now.",
        "Feels like one of those days where company might actually help.",
        "I'm in less of a mood to keep everyone at arm's length.",
        "A bit of familiar company would sit nicely right now.",
        "People don't feel quite as exhausting today.",
        "I could go for an easy conversation.",
        "I feel more open to being interrupted than usual.",
        "The idea of someone sitting nearby sounds comfortable rather than intrusive.",
        "I would not mind hearing a familiar laugh right now.",
        "I am in the mood to let a conversation run longer than necessary.",
        "It feels easier to be patient with other people today.",
        "I find myself hoping someone familiar wanders by.",
        "Even idle chatter sounds pleasant at the moment.",
        "I am less inclined to disappear into my own corner right now.",
        "A little company would make the time pass nicely.",
        "I feel more willing to share the quiet with someone.",
        "The thought of a familiar voice is oddly reassuring.",
        "I could sit with someone for a while without needing much from them.",
        "I am noticing the pleasant parts of having people around.",
        "It feels easier to smile at small things today.",
        "I would not mind being drawn into somebody else's conversation.",
        "There is a softness to my mood that makes people easier to tolerate.",
        "I feel more curious about what everyone else is doing.",
        "I am less protective of my personal space than usual.",
        "A simple conversation sounds surprisingly good right now.",
        "I feel like being around people without needing a reason for it.",
        "I would welcome a familiar face more than I normally admit.",
        "It feels like I have a little more room for other people today.",
        "I am not in much of a mood to keep things distant.",
        "I could listen to someone talk for a while and not mind at all.",
        "The room feels more inviting when there are voices in it.",
        "I am finding it easier to feel fond of familiar little habits.",
        "I would not mind sharing a drink or a table with someone right now.",
        "There is something nice about not feeling entirely self-contained.",
        "I feel more inclined to stay when someone starts talking.",
        "Other people feel less like work at the moment.",
        "I am in a mood where a quiet bit of company would be enough.",
        "I keep thinking how nice it would be to see somebody I know.",
        "I feel unusually receptive to a little warmth from someone else.",
        "A familiar presence would fit this mood nicely.",
        "I am not looking for solitude as hard as I usually do.",
        "It feels easier to let people a little closer today.",
        "I would not mind ending up in a long, pointless conversation.",
        "There is a comfortable pull toward company in me right now.",
    },
    IRRITATED={
        "Everything feels half a degree more annoying than it ought to.",
        "That irritation is still sitting there.",
        "I can feel my patience thinning.",
        "Small things are landing harder than they should.",
        "I'm getting annoyed at things I normally wouldn't notice.",
        "There's a rough edge on everything right now.",
        "My patience feels shorter than usual.",
        "I keep catching myself getting irritated over nothing much.",
        "Even harmless noise is starting to grate.",
        "Something in me is looking for an excuse to be annoyed.",
        "The smallest interruptions are getting on my nerves.",
        "I keep clenching my jaw without meaning to.",
        "Every repeated sound feels louder than the last one.",
        "I am more bothered by people taking their time than I usually am.",
        "I can feel annoyance arriving before I even know what caused it.",
        "I keep wanting the room to be quieter than it is.",
        "Little inconveniences are sticking with me longer than they deserve.",
        "I am finding it harder to shrug things off right now.",
        "My patience keeps catching on tiny things.",
        "I am in no mood for somebody to repeat themselves three times.",
        "I keep noticing habits in other people that I would normally ignore.",
        "Even waiting a few seconds feels more irritating than it should.",
        "I can feel myself answering things more sharply in my head.",
        "There is a tightness in my mood that will not quite loosen.",
        "I am getting tired of things before they have even properly started.",
        "Every small delay is testing me more than usual.",
        "I keep exhaling like that is somehow going to get rid of the irritation.",
        "My tolerance for unnecessary noise is very low right now.",
        "I am starting to resent how many little things need my attention.",
        "I keep wanting people to get to the point.",
        "The atmosphere feels more abrasive than it probably is.",
        "I am taking harmless things personally enough to notice it.",
        "My mood keeps finding new little places to snag.",
        "I can feel myself getting irritated just from being interrupted.",
        "I am not in the mood for anyone to make a simple thing complicated.",
        "The more I notice my irritation, the more irritating that becomes too.",
        "I keep rubbing at the same annoyance in my head.",
        "There is a low simmer under everything I am doing.",
        "I feel like I have less patience to spare than usual.",
        "Even polite small talk sounds a little exhausting right now.",
        "I keep catching myself wanting everyone to move a little faster.",
        "My nerves feel closer to the surface than I would like.",
        "I am irritated enough that I can hear it in my own thoughts.",
        "Things that usually roll off me are hanging around today.",
        "I keep wanting a few minutes without anybody asking anything of me.",
        "There is too much friction in my mood for no good reason.",
        "I feel one pointless inconvenience away from a bad mood.",
        "Everything around me seems to have found a slightly more annoying version of itself.",
    },
    REFLECTIVE={
        "My mind keeps wandering backward.",
        "Old moments keep resurfacing.",
        "Funny what the mind decides to drag back up.",
        "Bits of the past keep floating up without invitation.",
        "I keep replaying things that are already over.",
        "My head has decided this is a good time to remember everything.",
        "Old conversations keep arriving in pieces.",
        "Something from before keeps tugging at the edge of my attention.",
        "I keep circling back to things I thought I'd put down.",
        "Quiet always gives old memories too much room.",
        "I keep remembering details I did not think mattered at the time.",
        "An old expression keeps coming back to me for no clear reason.",
        "I am thinking about how differently something felt when it was happening.",
        "Certain words from before keep sounding different in hindsight.",
        "I keep wondering why one small memory has stayed so clear.",
        "The past feels unusually close today.",
        "I am remembering the pauses in old conversations more than the words.",
        "Something I dismissed at the time feels more noticeable now.",
        "I keep returning to an old choice without quite judging it.",
        "A memory I have not touched in a while keeps nudging its way forward.",
        "I am noticing how much some things have changed since then.",
        "I keep comparing the way things felt then to how they feel now.",
        "An old place has been sitting in the back of my mind for a while.",
        "I can almost hear a conversation that has been over for ages.",
        "I keep remembering what I did not say more than what I did.",
        "There are old details coming back in an oddly specific order.",
        "I am thinking about someone I have not had reason to think about lately.",
        "A small memory keeps opening into three others behind it.",
        "I keep turning over something that never really got a clean ending.",
        "The quiet is making room for things I usually leave alone.",
        "I am remembering how certain people used to sound when they were relaxed.",
        "Something from a while ago feels less settled than I thought it was.",
        "I keep noticing how much of the past survives in little habits.",
        "An old mistake has crossed my mind without bringing the usual sting with it.",
        "I am thinking about a time that felt ordinary until it was gone.",
        "I keep remembering the parts of things nobody else would have noticed.",
        "A familiar memory has come back with a detail I had forgotten.",
        "I am tracing an old conversation from beginning to end again.",
        "There is something comforting about remembering a moment exactly as it was.",
        "I keep wondering what I would notice now if I saw that moment again.",
        "My thoughts keep gathering around things that are already finished.",
        "I am more aware than usual of how much time has passed.",
        "An old face keeps appearing in my thoughts between unrelated things.",
        "I keep remembering the mood of a place more clearly than what happened there.",
        "Some memories feel quieter with distance, but not smaller.",
        "I am thinking about how easily something once felt normal.",
        "My mind keeps pulling loose threads from old moments.",
        "I have been carrying more of the past around today than I realized.",
    },
    FOCUSED={
        "Everything outside the immediate thing feels quieter.",
        "My attention has narrowed down nicely.",
        "Hard to think about much beyond what's right in front of me.",
        "My head feels unusually locked onto one thing.",
        "Distractions aren't sticking very well right now.",
        "Nice when everything unnecessary just falls to the edges.",
        "I can feel my attention settling into place.",
        "One thing at a time feels easy right now.",
        "My thoughts are moving in a straighter line than usual.",
        "Whatever else is happening can wait in the background for a bit.",
        "I keep noticing smaller details without losing the whole picture.",
        "My attention is staying exactly where I put it for once.",
        "The background noise is barely making it into my thoughts.",
        "I feel unusually patient with details right now.",
        "It is easy to keep following the same thread without drifting away.",
        "My mind feels cleaner when it only has one thing to hold.",
        "I am not having to drag my attention back every few seconds.",
        "The next detail keeps arriving before the last one slips away.",
        "I feel more precise than hurried at the moment.",
        "My thoughts are lining up instead of arriving all at once.",
        "I can stay with one thing long enough to actually notice it properly.",
        "It feels good to have my attention stop wandering for a while.",
        "I keep filtering out little distractions without effort.",
        "There is a steady rhythm to the way I am thinking right now.",
        "I am catching details I might normally skip over.",
        "My head feels occupied in a useful way.",
        "It is easier than usual to keep track of where I was.",
        "I feel less tempted to split my attention between everything around me.",
        "I keep following the same line of thought without losing it halfway through.",
        "The things around me feel simpler when I am this concentrated.",
        "I am noticing the order of things more clearly than usual.",
        "My attention feels settled rather than tense.",
        "I can hold onto a thought long enough to finish it properly.",
        "It feels like there is less static between one thought and the next.",
        "I keep returning to the same detail because it actually seems worth examining.",
        "I am not bored enough to look for another distraction yet.",
        "My mind feels narrower, but in a useful way.",
        "I am taking in more by looking at less.",
        "The rest of the room can move around without pulling me away from this.",
        "I feel unusually good at ignoring things that do not matter right now.",
        "My attention is sitting exactly where it needs to be.",
        "I keep catching myself going a little deeper into the same thought.",
        "There is something satisfying about following one thing all the way through.",
        "My mind feels steady enough to notice what usually gets missed.",
        "The immediate details are holding me without much effort.",
        "I am not fighting myself to stay interested right now.",
        "Everything unrelated feels a little farther away than usual.",
        "I have enough concentration left to be careful instead of quick.",
    },
    WEARY={
        "There is a weight behind my eyes.",
        "Everything feels a little heavier than usual.",
        "Tiredness is starting to sit in my bones.",
        "I could disappear into a chair for a while.",
        "My body feels a step behind my thoughts.",
        "I'm running on less energy than I'd like.",
        "Even standing still feels mildly exhausting.",
        "I can feel the tiredness collecting in my shoulders.",
        "A proper rest sounds better every minute.",
        "My eyes keep asking for a break.",
        "My limbs feel heavier than the amount of effort I am actually using.",
        "I keep blinking a little longer than necessary.",
        "My shoulders are starting to feel like they are carrying extra weight.",
        "Everything takes a fraction more effort than I expect it to.",
        "I can feel myself moving a little slower without deciding to.",
        "My thoughts are keeping up better than my body is.",
        "I would happily sit down somewhere quiet for a while.",
        "There is a dull tiredness behind everything else I am feeling.",
        "My eyes keep losing interest in staying fully open.",
        "I feel like I have been awake longer than I actually have.",
        "The idea of getting comfortable is becoming increasingly appealing.",
        "I keep stretching my neck as if that might shake some energy loose.",
        "My legs feel like they would appreciate doing less.",
        "The tiredness is not dramatic, just persistent.",
        "I am spending more effort than usual on looking awake.",
        "My body keeps reminding me that it would rather slow down.",
        "I feel a little less sharp around the edges than I normally do.",
        "Even simple movement has started to feel slightly expensive.",
        "I keep wanting to lean against the nearest solid thing.",
        "There is a slow drag in my attention that was not there earlier.",
        "My face feels tired in a way that is hard to hide from myself.",
        "I keep taking deeper breaths like they might wake me up.",
        "My reactions feel a half-beat slower than usual.",
        "The thought of a quiet room sounds better than almost anything else.",
        "I can feel the tiredness in the way I am holding my head.",
        "I am losing a little enthusiasm for anything that sounds like effort.",
        "My body feels like it has already done enough for one day.",
        "I keep noticing how comfortable every chair looks.",
        "There is a heaviness in my posture I cannot quite shake.",
        "I feel like I could close my eyes for a minute and lose track of time.",
        "My attention keeps softening at the edges.",
        "I am not exhausted, just noticeably low on energy.",
        "My shoulders keep dropping whenever I stop paying attention to them.",
        "I could use a long stretch and somewhere to sit afterward.",
        "The tiredness has settled into a steady background presence.",
        "I keep moving because stopping sounds dangerously comfortable.",
        "My eyes feel a little dry from being open this long.",
        "I am starting to feel the day in my bones.",
    },
    UNCOMFORTABLE={
        "That ache keeps stealing pieces of my attention.",
        "Pain has a way of making itself impossible to forget.",
        "Every so often that injury reminds me it's still there.",
        "I keep noticing the same sore spot again and again.",
        "Moving wrong makes that pain flare right back up.",
        "That discomfort keeps interrupting perfectly good thoughts.",
        "My body keeps dragging my attention back to the injury.",
        "Hard to ignore pain when it keeps changing how I hold myself.",
        "That ache has settled in like an unwanted passenger.",
        "I can feel myself compensating for it without thinking.",
        "I keep adjusting my posture to take pressure off the sore spot.",
        "There is no position that makes the discomfort disappear entirely.",
        "Every few movements I get another reminder of where it hurts.",
        "I keep testing the sore area without meaning to.",
        "My body has started arranging itself around the pain.",
        "The ache is quiet until I move the wrong way.",
        "I can feel tension building around the injured area.",
        "I keep avoiding certain movements before I consciously think about them.",
        "The discomfort is making me more aware of every small shift in my weight.",
        "I keep finding the edge of the pain and backing away from it.",
        "The sore spot is turning ordinary movement into a negotiation.",
        "I can tell I am protecting one side of myself more than the other.",
        "The pain is not constant, but it is never very far away either.",
        "I keep noticing how much effort goes into not aggravating it.",
        "My body keeps remembering the injury before my mind does.",
        "There is a dull pull there that refuses to fade into the background.",
        "I am holding myself differently just to keep it manageable.",
        "A small movement is enough to bring the ache back into focus.",
        "I keep becoming aware of the injury at inconvenient moments.",
        "The discomfort has made me much more careful with how I move.",
        "I can feel the surrounding muscles staying tighter than they need to.",
        "The ache keeps shifting between dull and sharp depending on how I move.",
        "I keep trying to find a position that does not put pressure on it.",
        "My attention keeps checking the injury like it expects something to change.",
        "It is difficult to forget a sore spot when clothing keeps brushing against it.",
        "I can feel myself slowing certain movements down without planning to.",
        "The injury is making ordinary posture feel strangely deliberate.",
        "I keep noticing the difference between the comfortable side and the sore one.",
        "There is a constant little calculation behind how I move right now.",
        "The pain is making me more aware of my own body than I want to be.",
        "I keep catching myself bracing before movements that might hurt.",
        "The sore area feels louder whenever everything else gets quiet.",
        "I am giving that part of my body more space than usual.",
        "I keep changing position and discovering a different kind of discomfort.",
        "The ache has become part of how I measure every movement.",
        "I can feel myself guarding the injury even when nobody is near me.",
        "The discomfort keeps pulling my attention down into my body.",
        "I am getting tired of being reminded of the same sore place.",
    },
}

local function recentThoughtMap(p)
    local seen={};local h=type(p and p.history)=="table"and p.history or{}
    for i=math.max(1,#h-99),#h do local r=h[i];if type(r)=="table"then if r.rawText then seen[tostring(r.rawText)]=true end;if r.text then seen[tostring(r.text)]=true end end end
    return seen
end
local function freshPool(pool,p)
    if not pool or#pool==0 then return nil end
    local seen=recentThoughtMap(p);local fresh={}
    for _,line in ipairs(pool)do if not seen[line]then fresh[#fresh+1]=line end end
    if#fresh==0 then return nil end
    return fresh[math.random(1,#fresh)]
end

local function stateThought(p)
    local st=computeLivingState(false)
    if st and RPAL.GeneratePersonaStateThought then
        local line,mind=RPAL.GeneratePersonaStateThought(st,p,"living")
        if line then return line,"state","living-state",mind,math.floor(clamp((tonumber(st.score)or .35)*58,0,58)+.5) end
    end
    local pool=st and SAFE_STATE_THOUGHTS[st.label];if not pool or#pool==0 then return nil end
    local line=freshPool(pool,p);if not line then return nil end
    return line,"state","living-state",nil,math.floor(clamp((tonumber(st.score)or .35)*58,0,58)+.5)
end

local DYNAMIC_POOLS={
    tense={"%s being here has me on edge.","I keep noticing where %s is.","Still not comfortable with %s this close.","I can feel my attention checking on %s without asking me first.","Having %s nearby is making it harder to fully relax.","I keep catching myself tracking %s around the room.","Something in me stays tighter whenever %s is close.","I don't love having %s within easy reach.","Even when I'm not looking at %s, I know roughly where they are.","%s being nearby is taking up more of my attention than I'd like."},
    warm={"There is something easier about having %s nearby.","Good to have %s around.","%s's presence feels familiar in a good way.","The room feels a little less dull with %s here.","I don't mind the quiet as much with %s nearby.","Something about having %s around lets me loosen up a little.","It's nice not having to wonder where %s is for once.","%s being here feels reassuring in a way I don't need to explain.","I keep noticing %s and feeling oddly glad they're still here.","Having %s nearby feels comfortably normal."},
    familiar={"I'm starting to recognize %s's little habits.","%s is becoming a familiar face.","Funny how quickly %s has started feeling familiar.","I keep noticing the small things %s tends to do.","%s doesn't feel quite like a stranger anymore.","I'm getting used to the rhythm of having %s around.","There are little things about %s I already recognize without thinking.","%s is starting to feel like part of the usual scenery.","I can tell I'm getting more used to %s being around.","The unfamiliar edges around %s are wearing down."},
    uncertain={"I keep noticing %s nearby.","%s is still a bit of an unknown quantity.","Not sure what I make of %s yet.","I haven't settled on an impression of %s.","Something about %s still feels undecided in my head.","I keep trying to get a read on %s without meaning to.","%s hasn't quite become familiar yet.","I wonder what another conversation with %s would change.","I still can't tell what sort of presence %s is going to become.","%s keeps catching my attention without giving me a clear reason why."},
}

local function dynamicThought(p)
    local list=RPAL.GetPersonaRelationshipDynamics(12,true)or{};if#list==0 then return nil end
    local ranked={}
    for _,d in ipairs(list)do
        local pressure=math.max(tonumber(d.tension)or 0,tonumber(d.warmth)or 0,tonumber(d.familiarity)or 0)
        local recent=math.min(.18,(tonumber(d.recentInteractions)or 0)*.025)+math.min(.12,(tonumber(d.recentSessions)or 0)*.035)
        ranked[#ranked+1]={d=d,score=(pressure+recent)*100}
    end
    table.sort(ranked,function(a,b)return(a.score or 0)>(b.score or 0)end)
    for _,candidate in ipairs(ranked)do
        local d=candidate.d;local name=trim(d.name)
        if name~=""then
            if RPAL.GeneratePersonaRelationshipThought then
                local line,mind=RPAL.GeneratePersonaRelationshipThought(d,p,"present")
                if line then return line,"social","relationship-dynamic",mind,candidate.score end
            end
            local pool
            if(d.tension or 0)>=.46 then pool=DYNAMIC_POOLS.tense
            elseif(d.warmth or 0)>=.48 then pool=DYNAMIC_POOLS.warm
            elseif(d.familiarity or 0)>=.35 then pool=DYNAMIC_POOLS.familiar
            else pool=DYNAMIC_POOLS.uncertain end
            local seen=recentThoughtMap(p);local options={}
            for _,fmt in ipairs(pool)do local line=string.format(fmt,name);if not seen[line]then options[#options+1]=line end end
            if#options>0 then return options[math.random(1,#options)],"social","relationship-dynamic",nil,candidate.score end
        end
    end
    return nil
end

local CONVERSATION_THOUGHTS={
    "Been talking with %s for a while now.",
    "Still going back and forth with %s.",
    "This talk with %s has found a rhythm.",
    "Still got things to say to %s, apparently.",
    "%s and I have been talking for a bit now.",
    "I'm still following where this goes with %s.",
    "This has turned into a proper back-and-forth with %s.",
    "Been at this with %s longer than I noticed.",
    "Still here talking with %s. Fair enough.",
    "The back-and-forth with %s is still going.",
    "I haven't run out of things to say to %s yet.",
    "Still keeping up with %s. Good.",
}
local function conversationThought(p)
    local c=RPAL.GetPersonaConversationContext and RPAL.GetPersonaConversationContext()or{}
    local name=trim(c.person);local turns=tonumber(c.turns)or 0
    if not c.active or not c.coherent or name==""or turns<4 or(tonumber(c.age)or 999)>75 then return nil end
    local seen=recentThoughtMap(p);local options={}
    for _,fmt in ipairs(CONVERSATION_THOUGHTS)do local line=string.format(fmt,name);if not seen[line]then options[#options+1]=line end end
    if#options==0 then return nil end
    local line=options[math.random(1,#options)]
    local score=46+math.min(12,turns*.75)+math.min(6,(tonumber(c.score)or 0)*6)
    return line,"conversation","conversation-state",{version=VERSION,lens="live-conversation",person=name,turns=turns,score=tonumber(c.score)or 0},score
end
RPAL.GeneratePersonaConversationStateThought=function(p)return conversationThought(p or(RPAL.GetPersonalityData and RPAL.GetPersonalityData()))end

local function memoryThought(p)
    local memories=RPAL.GetPersonaMemories(36)or{};if#memories==0 then return nil end
    -- Episodic recall is relevance-driven. No cue strong enough means no memory
    -- thought at all; Persona does not fill the slot with a random old name.
    if RPAL.GetPersonaBestEpisodeRecall then
        local candidate=RPAL.GetPersonaBestEpisodeRecall(memories,p)
        if candidate and candidate.memory and RPAL.GeneratePersonaMemoryThought then
            local line,mind=RPAL.GeneratePersonaMemoryThought(candidate.memory,p)
            if line then
                if mind then mind.recallScore=candidate.score;mind.recallReason="episode won present-context relevance scoring" end
                return line,"memory","character-memory",mind,candidate.score,candidate
            end
        end
    end
    return nil
end

local function looksLikeAdvice(line)
    local s=tostring(line or""):lower():gsub("[%c]"," "):gsub("%s+"," ")
    -- Word-boundary checks use punctuation-normalized text as well, so an old
    -- saved line such as "session," or "Persona." cannot slip through merely
    -- because punctuation touches the word.
    local padded=" "..s:gsub("[%p]"," "):gsub("%s+"," ").." "

    -- Persona thoughts must never expose ODYN/system vocabulary. Internal
    -- storage may still call a reflection a Session, but the character does
    -- not know that word as a gameplay concept.
    local systemWords={" session "," sessions "," roleplay "," persona "," calibration "," moral core "," renown "," addon "," odyn "," player "," savedvariables "," toast "}
    for _,pat in ipairs(systemWords)do if padded:find(pat,1,true)then return true end end

    -- Modal words are fine when they express a personal want/plan: "Maybe I
    -- should go fishing sometime" is ordinary inner speech. What we reject is
    -- coaching, moral instruction or generic guidance on how the character
    -- ought to behave.
    local secondPerson={" you should "," you ought "," you need to "," you have to "," you must "," you better "}
    for _,pat in ipairs(secondPerson)do if padded:find(pat,1,true)then return true end end
    local prescriptive={
        " ask before assuming "," ask them before "," before i assume "," before assuming ",
        " benefit of the doubt "," make sure nobody "," make sure everyone ",
        " be a little more patient "," be more patient "," give people a little room ",
        " try not to make this harder "," remember that people do not always ",
        " listen a little more before "," listen before explaining ",
        " pay attention to what people actually do "," assume confidence means honesty ",
        " trust my first reaction "," wait before deciding what i think ",
        " watch how this develops "," keep my options open ",
        " be honest with myself about "," decide instead of circling around ",
        " stop confusing waiting with progress "," supposed to be kinder ",
        " the right thing to do is "," the better person would "," people should ",
        " i should be kinder "," i should be nicer "," i should be more patient ",
        " i should forgive "," i should give them another chance "," i should hear them out ",
        " i should not assume the worst "," i should let that go "," i should keep this simple and clear ",
        " i should tell them plainly "," honesty is the kinder option "," patience is the better answer ",
        " i should remember "," i should probably remember "," i should notice "," i should probably notice ",
        " i should pay attention "," i should probably pay attention "," i should make sure "," i should probably make sure ",
        " i should trust "," i should probably trust "," i should stop treating "," i should probably be prepared ",
        " i should keep my word "," i should follow through "," i should do the sensible thing ",
    }
    for _,pat in ipairs(prescriptive)do if padded:find(pat,1,true)then return true end end

    -- Retire old dramatic aphorisms / "cool one-liners". Persona should sound
    -- like a person quietly thinking, not a trailer narrator or quote generator.
    local punchlines={
        " cruelty leaves a stain "," mercy feels different "," silence is not the same thing ",
        " some debts are "," every card "," being underestimated "," people reveal a lot ",
        " principles get blurry "," intent is nice "," that is the shape of it ",
        " some fights are just "," the easy answer feels "," restraint costs ",
        " somebody has to keep things in order "," the boring correct thing ",
        " no reason to show every card "," if somebody has to stand there ",
        " no point turning a simple problem into a crusade "," leverage than fairness ",
        " no sense showing every card "," some lines feel easier to cross ",
    }
    for _,pat in ipairs(punchlines)do if padded:find(pat,1,true)then return true end end
    return false
end
RPAL.PersonaThoughtLooksLikeAdvice=looksLikeAdvice
RPAL.PersonaThoughtPassesNaturalness=function(line)return not looksLikeAdvice(line)end

local function continuityWhy(source,mind,p)
    mind=type(mind)=="table"and mind or{}
    if source=="character-memory"and type(mind.episode)=="table"then
        local e=mind.episode;local interpretation=tostring(e.interpretationStatus or"unknown");local read="observed episode; inner motive remains unknown"
        if interpretation=="confirmed"and trim(e.interpretation)~=""then read="player-confirmed interpretation: "..tostring(e.interpretation)
        elseif interpretation=="inferred"and trim(e.interpretation)~=""then read="tentative interpretation: "..tostring(e.interpretation)end
        local evidence=tostring(e.anchor or"");if trim(e.behavior)~=""then evidence=evidence.."  •  observed response: "..tostring(e.behavior):gsub("%-"," ")end
        return{trigger="A stored Episode Card matched the present context strongly enough to be recalled.",evidence=evidence,appraisal=read,state=(RPAL.GetPersonaLivingState and(RPAL.GetPersonaLivingState()or{}).label)or"",confidence=tonumber(e.confidence)or 1,recallScore=tonumber(mind.recallScore)or 0,person=e.person}
    end
    if source=="conversation-state"then
        return{trigger="A real, currently owned conversation has stayed active long enough to register as part of the character's immediate experience.",evidence=trim(mind.person or""),appraisal="ongoing conversation / no extra semantic claim",state=(RPAL.GetPersonaLivingState and(RPAL.GetPersonaLivingState()or{}).label)or"",confidence=clamp(tonumber(mind.score)or .7,.45,1),person=mind.person}
    end
    if source=="relationship-dynamic"then
        return{trigger="A currently present or very recent person is exerting relationship pressure on the character.",evidence=trim(mind.person or""),appraisal="current relationship dynamic",state=(RPAL.GetPersonaLivingState and(RPAL.GetPersonaLivingState()or{}).label)or"",confidence=1}
    end
    if source=="living-state"then
        local st=RPAL.GetPersonaLivingState and RPAL.GetPersonaLivingState()or{}
        return{trigger="The character's current Living State was the strongest safe candidate.",evidence=tostring(st.detail or""),appraisal=tostring(st.label or"internal state"),state=tostring(st.label or""),confidence=1}
    end
end

local baseGenerate=RPAL.GeneratePersonalityThought
if baseGenerate then
    RPAL.GeneratePersonalityThought=function(context)
        if not ready(false)then return nil end
        local p=RPAL.GetPersonalityData and RPAL.GetPersonalityData();local raw,category,source,mind
        if tostring(context or"")~="preview"then
            local candidates={}
            local mline,mcat,msrc,mmind,mscore,mrecall=memoryThought(p)
            if mline then candidates[#candidates+1]={line=mline,category=mcat,source=msrc,mind=mmind,score=tonumber(mscore)or 42,priority=4,recall=mrecall}end
            local cline,ccat,csrc,cmind,cscore=conversationThought(p)
            if cline then candidates[#candidates+1]={line=cline,category=ccat,source=csrc,mind=cmind,score=tonumber(cscore)or 48,priority=5}end
            local dline,dcat,dsrc,dmind,dscore=dynamicThought(p)
            if dline then candidates[#candidates+1]={line=dline,category=dcat,source=dsrc,mind=dmind,score=tonumber(dscore)or 30,priority=3}end
            local sline,scat,ssrc,smind,sscore=stateThought(p)
            if sline then candidates[#candidates+1]={line=sline,category=scat,source=ssrc,mind=smind,score=tonumber(sscore)or 20,priority=2}end
            local conversation=RPAL.GetPersonaConversationContext and RPAL.GetPersonaConversationContext()or{}
            if conversation.active and conversation.coherent then
                for _,c in ipairs(candidates)do
                    if c.source=="conversation-state"then c.score=c.score+10
                    elseif c.source=="relationship-dynamic"then c.score=c.score+8
                    elseif c.source=="character-memory"then c.score=c.score+2
                    else c.score=c.score-18 end
                end
            end
            table.sort(candidates,function(a,b)if a.score==b.score then return(a.priority or 0)>(b.priority or 0)end;return(a.score or 0)>(b.score or 0)end)
            local winner=candidates[1]
            if winner and(winner.score>=38 or winner.source=="character-memory")then
                raw,category,source,mind=winner.line,winner.category,winner.source,winner.mind
                if winner.source=="character-memory"and winner.recall and RPAL.MarkPersonaEpisodeRecalled then RPAL.MarkPersonaEpisodeRecalled(winner.recall.episode,winner.recall.memory)end
            end
        end
        if raw then
            local rawText=raw
            local styled=RPAL.StylePersonaThought and RPAL.StylePersonaThought(rawText)or rawText
            if not looksLikeAdvice(rawText) and not looksLikeAdvice(styled) then
                local row={text=styled,rawText=rawText,category=category or"thought",source=source or"continuity",at=now(),context=tostring(context or"ambient"),mind=mind,why=continuityWhy(source,mind,p)}
                if p then p.history=type(p.history)=="table"and p.history or{};p.history[#p.history+1]=row;while#p.history>120 do table.remove(p.history,1)end end
                return styled,row
            end
        end
        for _=1,20 do
            local line,row=baseGenerate(context)
            if not line then return nil,row end
            local rawCandidate=row and row.rawText or line
            if not looksLikeAdvice(rawCandidate) and not looksLikeAdvice(line)then return line,row end
            if p and row and type(p.history)=="table"and p.history[#p.history]==row then table.remove(p.history)end
        end
        -- Twenty rejected candidates means the evidence/context gate did its job.
        -- Silence is better than forcing a generic filler thought into the scene.
        return nil
    end
end

-- Persona owns one thought cadence whether a Session exists or not. IC status is
-- enough. Scene.lua is told not to inject a second Persona thought through its
-- old Session-only spark path.
RPAL.PersonaContinuityOwnsThoughtCadence=true
function RPAL.MaybeShowAmbientPersonalityThought()return false end

local function scheduleThought(fresh)
    local t=(GetTime and GetTime())or 0
    local wait=RPAL.GetPersonalitySparkDelay and RPAL.GetPersonalitySparkDelay(fresh)or math.random(8*60,15*60)
    RPAL._personaContinuityNextThoughtAt=t+wait
end

local event=CreateFrame("Frame")
event:RegisterEvent("PLAYER_LOGIN")
event:RegisterEvent("PLAYER_ENTERING_WORLD")
local PERSONA_CONTINUITY_EVENTS={"CHAT_MSG_SAY","CHAT_MSG_EMOTE","PLAYER_TARGET_CHANGED","UPDATE_MOUSEOVER_UNIT"}
function RPAL.SetPersonaContinuityEventsEnabled(enabled)
    for _,eventName in ipairs(PERSONA_CONTINUITY_EVENTS) do
        if enabled then event:RegisterEvent(eventName) else event:UnregisterEvent(eventName) end
    end
    RPAL._personaContinuityEventsEnabled=enabled and true or false
end
RPAL.SetPersonaContinuityEventsEnabled(RPAL.IsPersonaSystemEnabled and RPAL.IsPersonaSystemEnabled() or false)
event:SetScript("OnEvent",function(_,ev,...)
    if ev=="PLAYER_LOGIN"or ev=="PLAYER_ENTERING_WORLD"then personaIdentityIndex=nil;personaIdentityIndexAt=-100;RPAL._personaContinuityNextThoughtAt=nil;computeLivingState(true);return end
    if not ready(true)then return end
    if ev=="PLAYER_TARGET_CHANGED"or ev=="UPDATE_MOUSEOVER_UNIT"then
        local unit=ev=="PLAYER_TARGET_CHANGED"and"target"or"mouseover"
        if UnitExists and UnitExists(unit)and UnitIsPlayer and UnitIsPlayer(unit)and not(UnitIsUnit and UnitIsUnit(unit,"player"))then
            local n=UnitName and UnitName(unit)
            if n then
                local display,row=personaIdentity(n)
                if display then
                    local d=ensureDynamic(display,row)
                    if d then syncDynamicRead(d,row);d.presentUntil=now()+3*60;d.lastAt=now()end
                end
            end
        end
        return
    end
    local msg,author=...;local guid=select(12,...);local mine=(guid and UnitGUID and guid==UnitGUID("player"))or normName(author)==normName(UnitName and UnitName("player")or"")
    if not mine and trim(author)~=""then touchDynamic(author,ev);RPAL.NudgePersonaLivingState("social",.025)end
end)

local function PersonaContinuityTick()
    if not ready(true)then RPAL._personaContinuityNextThoughtAt=nil;return end
    computeLivingState(false)
    local t=(GetTime and GetTime())or 0
    if not RPAL._personaContinuityNextThoughtAt then scheduleThought(true);return end
    if t<RPAL._personaContinuityNextThoughtAt then return end
    local thought,row=RPAL.GeneratePersonalityThought and RPAL.GeneratePersonalityThought("ambient")
    if thought and RPAL.ShowSceneSpark then RPAL.ShowSceneSpark(thought,row)end
    scheduleThought(false)
end
if C_Timer and C_Timer.NewTicker then
    C_Timer.NewTicker(20,PersonaContinuityTick)
else
    local accum=0
    event:SetScript("OnUpdate",function(_,elapsed)
        accum=accum+(elapsed or 0);if accum<20 then return end;accum=0
        PersonaContinuityTick()
    end)
end


-- ---------------------- SESSION REFLECTION PANEL -------------------------
-- Session recap always gets the first word. This panel opens only after the
-- recap closes, replacing the old long-lived SESSION REFLECTION toast.
local reflectionPanel
local WHITE="Interface\\Buttons\\WHITE8X8"
local FONT=STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF"
local RC={bg={.010,.009,.022,.995},panel={.026,.021,.047,.99},panel2={.037,.029,.064,.99},border={.29,.22,.47,.92},gold={.91,.71,.35,1},pale={.93,.94,.98,1},muted={.61,.63,.72,1},teal={.33,.79,.77,1},green={.31,.82,.50,1},red={.93,.38,.43,1}}
local function rbackdrop(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 rtext(parent,value,size,color)local fs=parent:CreateFontString(nil,"OVERLAY");fs:SetFont(FONT,size or 11,"");fs:SetText(tostring(value or""));color=color or RC.pale;fs:SetTextColor(color[1],color[2],color[3],color[4]or 1);if RPAL.RegisterScalableFontObject then RPAL.RegisterScalableFontObject(fs,FONT,size or 11,"")end;return fs end
local function rbutton(parent,label,w,h)local b=CreateFrame("Button",nil,parent,"BackdropTemplate");b:SetSize(w or 112,h or 32);rbackdrop(b,RC.panel2,RC.border);b.label=rtext(b,label,10.5,RC.pale);b.label:SetPoint("CENTER");b:SetScript("OnEnter",function(self)self:SetBackdropBorderColor(RC.gold[1],RC.gold[2],RC.gold[3],1)end);b:SetScript("OnLeave",function(self)self:SetBackdropBorderColor(RC.border[1],RC.border[2],RC.border[3],RC.border[4])end);return b end
local function rcard(parent,x,y,w,h,title)local c=CreateFrame("Frame",nil,parent,"BackdropTemplate");c:SetPoint("TOPLEFT",x,-y);c:SetSize(w,h);rbackdrop(c,RC.panel,RC.border);local t=rtext(c,title,10.5,RC.gold);t:SetPoint("TOPLEFT",14,-12);local rule=c:CreateTexture(nil,"ARTWORK");rule:SetTexture(WHITE);rule:SetHeight(1);rule:SetPoint("TOPLEFT",14,-34);rule:SetPoint("TOPRIGHT",-14,-34);rule:SetVertexColor(RC.gold[1],RC.gold[2],RC.gold[3],.27);return c end
local function pct(v)return math.floor((tonumber(v)or 0)*100+.5)end
local function durationText(sec)sec=math.max(0,math.floor(tonumber(sec)or 0));local m=math.floor(sec/60);local h=math.floor(m/60);m=m%60;if h>0 then return h.."h "..m.."m"end;return m.." min"end
local function relationRead(r)
    if not r then return"No strong relationship imprint."end
    local dw,dt,df=tonumber(r.deltaWarmth)or 0,tonumber(r.deltaTension)or 0,tonumber(r.deltaFamiliarity)or 0
    if dt>.025 and dt>=dw then return"Tension increased"elseif dw>.025 then return"Warmth increased"elseif df>.02 then return"Felt more familiar"elseif dt<-.025 then return"Tension eased"elseif dw<-.025 then return"Warmth cooled"end
    if(r.tension or 0)>.42 then return"Still carries tension"elseif(r.warmth or 0)>.42 then return"Feels distinctly warm"elseif(r.familiarity or 0)>.30 then return"Increasingly familiar"end
    return"A quiet impression remained"
end
local function ensureReflectionPanel()
    if reflectionPanel then return reflectionPanel end
    local f=CreateFrame("Frame","ODYN_PersonaSessionReflection",UIParent,"BackdropTemplate");reflectionPanel=f;f:SetSize(930,650);f:SetPoint("CENTER");f:SetFrameStrata("FULLSCREEN_DIALOG");f:SetFrameLevel(9890);f:SetToplevel(true);f:SetClampedToScreen(true);f:EnableMouse(true);rbackdrop(f,RC.bg,RC.gold);f:Hide()
    f.kicker=rtext(f,"PERSONA BETA  •  SESSION REFLECTION",9.5,RC.gold);f.kicker:SetPoint("TOPLEFT",26,-22)
    f.title=rtext(f,"",22,RC.pale);f.title:SetPoint("TOPLEFT",26,-44);f.title:SetWidth(690);f.title:SetWordWrap(false)
    f.meta=rtext(f,"",9.5,RC.muted);f.meta:SetPoint("TOPLEFT",27,-75)
    local close=rbutton(f,"CLOSE",86,28);close:SetPoint("TOPRIGHT",-20,-18);close:SetScript("OnClick",function()f:Hide()end)
    local reflection=rcard(f,24,105,882,126,"WHAT LINGERED")
    f.reflection=rtext(reflection,"",13.2,RC.pale);f.reflection:SetPoint("TOPLEFT",16,-49);f.reflection:SetPoint("TOPRIGHT",-16,-49);f.reflection:SetHeight(64);f.reflection:SetWordWrap(true);f.reflection:SetJustifyV("TOP")
    local state=rcard(f,24,247,278,150,"LIVING STATE")
    f.stateShift=rtext(state,"",15.5,RC.teal);f.stateShift:SetPoint("TOPLEFT",15,-50);f.stateShift:SetWidth(248)
    f.stateDetail=rtext(state,"",10.2,RC.muted);f.stateDetail:SetPoint("TOPLEFT",15,-82);f.stateDetail:SetWidth(248);f.stateDetail:SetHeight(54);f.stateDetail:SetWordWrap(true);f.stateDetail:SetJustifyV("TOP")
    local moral=rcard(f,326,247,278,150,"MORAL CORE")
    f.moralShift=rtext(moral,"",12.5,RC.gold);f.moralShift:SetPoint("TOPLEFT",15,-49);f.moralShift:SetWidth(248)
    f.moralValues=rtext(moral,"",10.1,RC.pale);f.moralValues:SetPoint("TOPLEFT",15,-79);f.moralValues:SetWidth(248);f.moralValues:SetHeight(57);f.moralValues:SetWordWrap(true)
    local social=rcard(f,628,247,278,252,"RELATIONSHIP IMPRINTS")
    f.socialRows={}
    for i=1,4 do local n=rtext(social,"",10.4,RC.pale);n:SetPoint("TOPLEFT",15,-49-(i-1)*47);n:SetWidth(246);local d=rtext(social,"",8.9,RC.muted);d:SetPoint("TOPLEFT",15,-67-(i-1)*47);d:SetWidth(246);f.socialRows[i]={name=n,detail=d}end
    local memories=rcard(f,24,413,580,168,"MEMORIES FORMED")
    f.memoryRows={}
    for i=1,4 do local m=rtext(memories,"",9.6,i==1 and RC.pale or RC.muted);m:SetPoint("TOPLEFT",16,-49-(i-1)*29);m:SetWidth(548);m:SetWordWrap(false);f.memoryRows[i]=m end
    f.note=rtext(f,"This reflection is stored in Character Memory and can influence later thoughts. It never performs an action for you.",9.2,RC.muted);f.note:SetPoint("BOTTOMLEFT",26,28);f.note:SetWidth(620)
    local open=rbutton(f,"OPEN PERSONA",142,32);open:SetPoint("BOTTOMRIGHT",-122,20);open:SetScript("OnClick",function()f:Hide();if RPAL.OpenPersonalityUI then RPAL.OpenPersonalityUI()end end)
    local done=rbutton(f,"DONE",92,32);done:SetPoint("BOTTOMRIGHT",-20,20);done:SetScript("OnClick",function()f:Hide()end)
    if RPAL.RegisterEscapeFrame then RPAL.RegisterEscapeFrame(f)elseif UISpecialFrames then table.insert(UISpecialFrames,"ODYN_PersonaSessionReflection")end
    return f
end
function RPAL.OpenPersonaSessionReflectionPanel(row)
    if not row or not ready(false)then return false end
    local f=ensureReflectionPanel();f._row=row
    f.title:SetText(tostring(row.title or"Session reflection"));f.meta:SetText(durationText(row.duration).."  •  "..date("%d %b %Y",tonumber(row.at)or now()))
    f.reflection:SetText(tostring(row.text or"Something from the session is still lingering."))
    local sb=type(row.stateBefore)=="table"and row.stateBefore or{label="CALM",detail=""};local sa=type(row.stateAfter)=="table"and row.stateAfter or{label=tostring(row.state or"CALM"),detail=""}
    f.stateShift:SetText(tostring(sb.label or"CALM").."  >  "..tostring(sa.label or"CALM"));f.stateDetail:SetText(tostring(sa.detail or""))
    local ab=type(row.alignmentBefore)=="table"and row.alignmentBefore or{};local aa=type(row.alignmentAfter)=="table"and row.alignmentAfter or{}
    f.moralShift:SetText(row.toward and("Shifted toward "..tostring(row.toward))or"No meaningful shift")
    f.moralValues:SetText(string.format("GOOD     %d%% > %d%%\nNEUTRAL  %d%% > %d%%\nEVIL     %d%% > %d%%",pct(ab.good),pct(aa.good),pct(ab.neutral),pct(aa.neutral),pct(ab.evil),pct(aa.evil)))
    local rels=type(row.relationshipChanges)=="table"and row.relationshipChanges or{}
    for i,ui in ipairs(f.socialRows)do local r=rels[i];ui.name:SetText(r and tostring(r.name or"Unknown")or(i==1 and"No character left a strong imprint."or""));ui.detail:SetText(r and relationRead(r)or"")end
    local mems=type(row.memoriesFormed)=="table"and row.memoriesFormed or{}
    for i,fs in ipairs(f.memoryRows)do local v=mems[i];fs:SetText(v and("•  "..tostring(v))or(i==1 and"No durable memory was formed beyond the session reflection."or""))end
    f:Show();f:Raise();return true
end
function RPAL.MaybeOpenPendingPersonaSessionReflection(sceneID)
    local row=RPAL._personaPendingSessionReflection;if not row then return false end
    if sceneID and row.sceneID and tostring(sceneID)~=tostring(row.sceneID)then return false end
    RPAL._personaPendingSessionReflection=nil
    return RPAL.OpenPersonaSessionReflectionPanel(row)
end
local previousShowPersonaUrge=RPAL.ShowPersonaUrge
if previousShowPersonaUrge then
    RPAL.ShowPersonaUrge=function(message,activityLabel)
        local result=previousShowPersonaUrge(message,activityLabel)
        if ready(true)and RPAL.NudgePersonaLivingState then RPAL.NudgePersonaLivingState("settled",.035)end
        return result
    end
end
