local ADDON_NAME, RPAL = ...

-- ============================================================
-- ODYN HEALTH 2.0
-- Character health history, anatomy presets, recovery stages,
-- treatment consent, per-record privacy, DM awareness and HUD.
-- Built on top of the stable Health drag/scale/rotate renderer.
-- ============================================================

local H2={mode="active",pending={},incoming={},lastDMState=false,shadow={},collapsedDays={}}
RPAL.Health2=H2

local PREFIX="ODYNHL2"
local STAGES={"fresh","treated","recovering","healed"}
local SEVERITIES={"minor","moderate","severe","critical"}
local PRIVACY={"public","odyn","private"}
local REGIONS={"Auto","Head","Neck","Chest","Upper Back","Abdomen","Lower Back","Left Shoulder","Right Shoulder","Left Upper Arm","Right Upper Arm","Left Forearm","Right Forearm","Left Hand","Right Hand","Hips","Left Thigh","Right Thigh","Left Knee","Right Knee","Left Lower Leg","Right Lower Leg","Left Foot","Right Foot"}
local PRESETS={"Humanoid","Broad Humanoid","Compact","Bestial","Hooved","Draconic","Custom"}
local PRESET_STYLE={
    ["Humanoid"]={w=1,h=1},["Broad Humanoid"]={w=1.12,h=.99},["Compact"]={w=.90,h=.88},
    ["Bestial"]={w=1.08,h=1.02},["Hooved"]={w=1.04,h=1.02},["Draconic"]={w=.98,h=1.05},["Custom"]={w=1,h=1},
}
local PRESET_TRAITS={
    ["Bestial"]={tail=true},["Hooved"]={tail=true,hooves=true,horns=true},["Draconic"]={tail=true,wings=true,horns=true},
}
local RACE_PRESET={
    Human="Humanoid",Scourge="Humanoid",NightElf="Humanoid",BloodElf="Humanoid",VoidElf="Humanoid",HighmountainTauren="Hooved",
    Dwarf="Compact",DarkIronDwarf="Compact",Gnome="Compact",Mechagnome="Compact",Goblin="Compact",Vulpera="Compact",
    Orc="Broad Humanoid",MagharOrc="Broad Humanoid",KulTiran="Broad Humanoid",Pandaren="Broad Humanoid",
    Worgen="Bestial",Tauren="Hooved",Draenei="Hooved",LightforgedDraenei="Hooved",Dracthyr="Draconic",EarthenDwarf="Compact",
}

local function Trim(s)return tostring(s or ""):gsub("^%s+",""):gsub("%s+$","") end
local function Clamp(v,a,b)v=tonumber(v)or 0;return math.max(a,math.min(b,v))end
local function Copy(t)local o={};for k,v in pairs(t or {})do if type(v)=="table"then o[k]=Copy(v)else o[k]=v end end;return o end
local HEALTH_PALETTE_DEFAULTS={
    gold={.86,.63,.28,1},pale={.9,.86,.76,1},muted={.55,.51,.45,1},
    panel={.055,.045,.035,.98},panel2={.085,.07,.055,.98},border={.30,.24,.16,1},
    red={.78,.2,.16,1},heal={.24,.68,.4,1},alteration={.72,.5,.96,1},green={.24,.7,.42,1},white={1,1,1,1},
}
local function Palette()
    -- Theme palettes only contain the generic ODYN colours.  Health adds
    -- semantic colours (red/heal/alteration/etc.), so always merge the
    -- current theme over a complete Health palette instead of returning the
    -- theme table directly.  Returning it directly made p.red/p.heal nil and
    -- the Medical History renderer crashed in Backdrop(unpack(nil)).
    local out=Copy(HEALTH_PALETTE_DEFAULTS)
    if RPAL.GetCurrentThemePalette then
        local themed=RPAL.GetCurrentThemePalette()
        if type(themed)=="table" then
            for k,v in pairs(themed)do
                if type(v)=="table" then out[k]=Copy(v) else out[k]=v end
            end
        end
    end
    return out
end
local function Backdrop(f,bg,border)
    local p=Palette();bg=type(bg)=="table"and bg or p.panel2;border=type(border)=="table"and border or p.border
    f:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8",edgeFile="Interface\\Buttons\\WHITE8X8",edgeSize=1,insets={left=0,right=0,top=0,bottom=0}})
    f:SetBackdropColor(unpack(bg));f:SetBackdropBorderColor(unpack(border))
end
local function Font(parent,text,size,color)
    local f=parent:CreateFontString(nil,"ARTWORK","GameFontNormal");local path=select(1,f:GetFont())or"Fonts\\FRIZQT__.TTF";local n=RPAL.GetScaledFontSize and RPAL.GetScaledFontSize(size or 10)or(size or 10)
    f:SetFont(path,n,"OUTLINE");f:SetText(text or "");f:SetTextColor(unpack(color or Palette().pale));return f
end
local function Button(parent,text,w,h)
    local p=Palette();local b=CreateFrame("Button",nil,parent,"BackdropTemplate");b:SetSize(w or 90,h or 26);b:EnableMouse(true);b:RegisterForClicks("LeftButtonUp");Backdrop(b,p.panel2,p.border);local t=Font(b,text,8.5,p.pale);t:SetPoint("CENTER");b.label=t
    b:SetScript("OnEnter",function(self)self:SetBackdropBorderColor(unpack(p.gold))end);b:SetScript("OnLeave",function(self)self:SetBackdropBorderColor(unpack(p.border))end);if RPAL.AttachButtonMotion then RPAL.AttachButtonMotion(b,0.018) end;if RPAL.AttachFocusMark then RPAL.AttachFocusMark(b,p.gold) end;return b
end
local function Edit(parent,w,h,multi)
    local p=Palette();local e=CreateFrame("EditBox",nil,parent,"BackdropTemplate");e:SetSize(w,h);e:EnableMouse(true);Backdrop(e,p.panel2,p.border);e:SetFont(STANDARD_TEXT_FONT,10,"OUTLINE");e:SetTextColor(unpack(p.pale));e:SetTextInsets(7,7,5,5);e:SetAutoFocus(false);e:SetMultiLine(multi and true or false);e:SetScript("OnEscapePressed",function(self)self:ClearFocus()end);return e
end
local function Toast(msg)
    if RPAL.ShowToast then RPAL.ShowToast(tostring(msg or "")) elseif DEFAULT_CHAT_FRAME then DEFAULT_CHAT_FRAME:AddMessage("|cff8de7ffODYN Health:|r "..tostring(msg or "")) end
end

local historySerial=0
local function NewHistoryID(at)
    historySerial=historySerial+1
    return "h"..tostring(math.floor(tonumber(at) or time())).."_"..tostring(historySerial).."_"..tostring(math.random(100,999))
end

local function DB()
    local d=RPAL.EnsureInjuryDB and RPAL.EnsureInjuryDB() or nil
    if not d then return nil end
    d.health2Version=2
    d.history=type(d.history)=="table" and d.history or {}
    for index,e in ipairs(d.history) do
        if type(e)=="table" and not e.id then
            e.id=NewHistoryID((tonumber(e.at) or time())+index)
        end
    end
    d.customAlterations=type(d.customAlterations)=="table" and d.customAlterations or {}
    d.anatomy=type(d.anatomy)=="table" and d.anatomy or {}
    if not d.anatomy.preset then
        local _,raceFile=UnitRace("player");d.anatomy.preset=RACE_PRESET[raceFile] or "Humanoid"
    end
    d.anatomy.traits=type(d.anatomy.traits)=="table" and d.anatomy.traits or Copy(PRESET_TRAITS[d.anatomy.preset] or {})
    for _,item in pairs(d.items or {}) do
        item.privacy=(item.privacy=="private" and "private")or(item.privacy=="odyn" and "odyn")or"public"
        if item.category=="injury" then
            item.stage=({fresh=true,treated=true,recovering=true,healed=true})[item.stage] and item.stage or "fresh"
            item.severity=({minor=true,moderate=true,severe=true,critical=true})[item.severity] and item.severity or "moderate"
            item.stabilized=item.stabilized and true or nil
        end
        item.symptoms=type(item.symptoms)=="table" and item.symptoms or {}
        item.medicalNotes=type(item.medicalNotes)=="table" and item.medicalNotes or {}
        -- Health 2.0c briefly tagged DM injuries as encounter-temporary.
        -- From this build onward accepted DM proposals are ordinary persistent
        -- character Health records, so migrate any old tag away.
        if item.temporaryDM then item.temporaryDM=nil end
    end
    return d
end
RPAL.GetHealth2DB=DB

local function ItemLabel(item)
    if not item then return "Health Record" end
    if item.category=="alteration" then
        local custom=Trim(item.customName)
        local kind=Trim(item.type)
        if custom~="" then return custom end
        if kind~="" then return kind end
        return "Permanent Change"
    end
    local healing=Trim(item.healing)
    local kind=Trim(item.type)
    if healing~="" then return healing end
    if kind~="" then return kind end
    if item.category=="injury" then return "Injury" end
    if item.category=="healing" then return "Healing Status" end
    return "Health Record"
end
local function ActiveScene()
    return RPAL.GetActiveScene and RPAL.GetActiveScene() or nil
end
local function HistoryPrivacy(item)return item and item.privacy or "odyn" end
function RPAL.HealthAddHistory(kind,item,fromValue,toValue,actor,note)
    local d=DB();if not d then return end
    local now=time();local scene=ActiveScene();local e={id=NewHistoryID(now),at=now,kind=tostring(kind or "update"),itemID=item and tostring(item.id)or nil,label=ItemLabel(item),from=tostring(fromValue or ""),to=tostring(toValue or ""),actor=Trim(actor),note=Trim(note),privacy=HistoryPrivacy(item),sceneID=scene and scene.id or nil,sceneTitle=scene and scene.title or nil}
    table.insert(d.history,1,e);while #d.history>180 do table.remove(d.history)end
end

local function RegionFromXY(item)
    local x,y=tonumber(item and item.x)or .5,tonumber(item and item.y)or .5;local back=item and item.side=="back"
    if y<.16 then return "Head" end
    if y<.23 then return "Neck" end
    if x<.28 and y<.37 then return "Left Shoulder" end
    if x>.72 and y<.37 then return "Right Shoulder" end
    if x<.24 then if y<.47 then return "Left Upper Arm" elseif y<.64 then return "Left Forearm" else return "Left Hand" end end
    if x>.76 then if y<.47 then return "Right Upper Arm" elseif y<.64 then return "Right Forearm" else return "Right Hand" end end
    if y<.42 then return back and "Upper Back" or "Chest" end
    if y<.61 then return back and "Lower Back" or "Abdomen" end
    if y<.68 then return "Hips" end
    local left=x<.5
    if y<.80 then return left and "Left Thigh" or "Right Thigh" end
    if y<.86 then return left and "Left Knee" or "Right Knee" end
    if y<.96 then return left and "Left Lower Leg" or "Right Lower Leg" end
    return left and "Left Foot" or "Right Foot"
end
function RPAL.HealthGetRegion(item)return (item and Trim(item.regionOverride)~="" and item.regionOverride)or RegionFromXY(item)end

local function SymptomsText(item)
    local out={};for _,v in ipairs(item and item.symptoms or {})do if Trim(v)~="" then out[#out+1]=Trim(v)end end;return table.concat(out,", ")
end
local function ParseSymptoms(text)
    local out,seen={},{};for raw in tostring(text or ""):gmatch("[^,]+")do local v=Trim(raw);local k=v:lower();if v~="" and not seen[k] then seen[k]=true;out[#out+1]=v;if #out>=10 then break end end end;return out
end

local function HealthCounts()
    local d=DB();local c={injuries=0,recovering=0,permanent=0,critical=0,healed=0}
    for _,i in pairs(d and d.items or {})do
        if i.category=="injury" then
            if i.stage=="healed" then c.healed=c.healed+1 else c.injuries=c.injuries+1 end
            if i.stage=="recovering" or i.stage=="treated" then c.recovering=c.recovering+1 end
            if i.stage~="healed" and i.severity=="critical"then c.critical=c.critical+1 end
        elseif i.category=="alteration"then c.permanent=c.permanent+1 end
    end
    return c
end
function RPAL.GetHealthHUDSummary()
    local c=HealthCounts();local text=string.format("%d Injuries • %d Recovering • %d Permanent Changes",c.injuries,c.recovering,c.permanent)
    if c.healed>0 then text=text.." • "..tostring(c.healed).." Awaiting Aftermath" end
    return text,c
end

-- ---------- stable diff/history capture ----------
local function Shadow()
    local d=DB();local s={};for id,i in pairs(d and d.items or {})do s[id]={label=ItemLabel(i),category=i.category,type=i.type,privacy=i.privacy or "odyn"} end;return s
end
H2.shadow=Shadow()
local baseRefresh=RPAL.RefreshInjuryUI
if baseRefresh then
    RPAL.RefreshInjuryUI=function(...)
        local before=H2.shadow or {};local result={baseRefresh(...)};local after=Shadow();local d=DB()
        for id,v in pairs(after)do if not before[id]then local i=d.items[id];RPAL.HealthAddHistory("added",i,"",i and (i.category or "record")or"record",nil,"Placed on body map")end end
        for id,v in pairs(before)do if not after[id]then RPAL.HealthAddHistory("removed",{id=id,type=v.label,category=v.category,privacy=v.privacy or "odyn"},v.label,"Removed",nil,"")end end
        H2.shadow=after;if RPAL.RefreshHealthHUD then RPAL.RefreshHealthHUD()end;return unpack(result)
    end
end

-- ---------- anatomy ----------
local function Anatomy()local d=DB();return d and d.anatomy end
function RPAL.GetHealthCustomAlterations()local d=DB();return d and d.customAlterations or {}end
local function AddTraitLine(body,x1,y1,x2,y2,color)
    if not body or not body.CreateLine then return end
    body._healthTraitLines=body._healthTraitLines or {};local line=body:CreateLine(nil,"ARTWORK");line:SetThickness(2);line:SetColorTexture(color[1],color[2],color[3],.72);line:SetStartPoint("CENTER",body,"TOPLEFT",x1*body:GetWidth(),-y1*body:GetHeight());line:SetEndPoint("CENTER",body,"TOPLEFT",x2*body:GetWidth(),-y2*body:GetHeight());body._healthTraitLines[#body._healthTraitLines+1]=line
end
local function ClearTraits(body)for _,l in ipairs(body and body._healthTraitLines or {})do l:Hide()end;if body then body._healthTraitLines={}end end
local function DrawTraits(body,traits,side,color)
    ClearTraits(body);if not body then return end
    if traits.horns then AddTraitLine(body,.46,.08,.39,.00,color);AddTraitLine(body,.54,.08,.61,.00,color)end
    if traits.wings then
        AddTraitLine(body,.42,.29,.16,.20,color);AddTraitLine(body,.16,.20,.06,.42,color);AddTraitLine(body,.58,.29,.84,.20,color);AddTraitLine(body,.84,.20,.94,.42,color)
    end
    if traits.tail then
        if side=="back"then AddTraitLine(body,.50,.58,.66,.72,color);AddTraitLine(body,.66,.72,.72,.88,color)else AddTraitLine(body,.52,.58,.68,.74,color);AddTraitLine(body,.68,.74,.61,.90,color)end
    end
    if traits.hooves then AddTraitLine(body,.38,.97,.31,.99,color);AddTraitLine(body,.62,.97,.69,.99,color)end
end
local function ApplyAnatomyTo(body,image,side)
    if not body or not image then return end;local a=Anatomy()or{};local st=PRESET_STYLE[a.preset]or PRESET_STYLE.Humanoid
    image:ClearAllPoints();image:SetPoint("CENTER",body,"CENTER",0,0);image:SetSize(body:GetWidth()*st.w,body:GetHeight()*st.h);image:SetTexCoord(side=="back" and 1 or 0,side=="back" and 0 or 1,0,1)
    local p=Palette();local traits=Copy(PRESET_TRAITS[a.preset]or{});for k,v in pairs(a.traits or{})do traits[k]=v and true or false end;DrawTraits(body,traits,side,p.gold)
end
function RPAL.RefreshHealthAnatomyVisual()
    local ui=RPAL.InjuryUI;if not ui then return end;ApplyAnatomyTo(ui.body,ui.frontImage,"front");ApplyAnatomyTo(ui.backBody,ui.backImage,"back")
    if ui.anatomyButton then ui.anatomyButton.label:SetText("BODY: "..string.upper((Anatomy()and Anatomy().preset)or"Humanoid"))end
    if ui.traitButtons then for k,b in pairs(ui.traitButtons)do b:SetAlpha((Anatomy().traits or{})[k] and 1 or .48)end end
end
local function Cycle(list,value)
    local idx=1;for i,v in ipairs(list)do if v==value then idx=i break end end;idx=idx%#list+1;return list[idx]
end
function RPAL.CycleAnatomyPreset()
    local a=Anatomy();local old=a.preset;a.preset=Cycle(PRESETS,a.preset or"Humanoid");a.traits=Copy(PRESET_TRAITS[a.preset]or{});RPAL.HealthAddHistory("anatomy",nil,old,a.preset,nil,"Body map preset changed");RPAL.RefreshHealthAnatomyVisual()
end
function RPAL.ToggleAnatomyTrait(key)
    local a=Anatomy();a.traits=a.traits or{};a.traits[key]=not a.traits[key];RPAL.RefreshHealthAnatomyVisual()
end

local function DropdownOptions(raw)
    local out={}
    for _,entry in ipairs(raw or {}) do
        if type(entry)=="table" then
            out[#out+1]={value=entry.value~=nil and entry.value or entry[1],label=tostring(entry.label or entry[2] or entry.value or entry[1] or "")}
        else
            out[#out+1]={value=entry,label=tostring(entry)}
        end
    end
    return out
end

-- Lightweight ODYN dropdown used by Health modals.  It intentionally does not
-- use UIDropDownMenu: Blizzard's global dropdown frames can end up beneath our
-- TOOLTIP-strata modal root and steal/lose mouse focus.  This menu stays inside
-- the modal's own frame stack and supports long lists through a scroll frame.
local function Dropdown(parent,text,width,options,onSelect)
    local p=Palette()
    local b=Button(parent,text,width or 190,27)
    b._options=options
    b._popup=nil
    local function ClosePopup()
        local popup=b._popup
        if popup then popup:Hide() end
        if H2.openDropdown==popup then H2.openDropdown=nil end
        b._popup=nil
    end
    local function OpenPopup()
        if RPAL.CloseOpenDropdown then RPAL.CloseOpenDropdown(b._popup) end
        if H2.openDropdown and H2.openDropdown~=b._popup then
            H2.openDropdown:Hide()
            H2.openDropdown=nil
        end
        ClosePopup()
        local raw=type(b._options)=="function" and b._options() or b._options
        local opts=DropdownOptions(raw)
        if #opts==0 then return end
        local maxVisible=math.min(#opts,10)
        local popup=CreateFrame("Frame",nil,parent,"BackdropTemplate")
        b._popup=popup
        H2.openDropdown=popup
        if RPAL.SetOpenDropdown then RPAL.SetOpenDropdown(popup) end
        popup:SetWidth((width or 190)+20)
        popup:SetHeight(maxVisible*25+10)
        popup:SetPoint("TOPLEFT",b,"BOTTOMLEFT",0,-2)
        popup:SetFrameLevel(math.max(parent:GetFrameLevel()+60,b:GetFrameLevel()+40))
        popup:EnableMouse(true)
        Backdrop(popup,p.panel,p.gold)
        popup:HookScript("OnHide",function(self)
            if H2.openDropdown==self then H2.openDropdown=nil end
            if b._popup==self then b._popup=nil end
        end)

        local scroll=CreateFrame("ScrollFrame",nil,popup,"UIPanelScrollFrameTemplate")
        if RPAL.SkinScrollFrame then RPAL.SkinScrollFrame(scroll) end
        scroll:SetPoint("TOPLEFT",4,-4)
        scroll:SetPoint("BOTTOMRIGHT",-24,4)
        scroll:EnableMouseWheel(true)
        local content=CreateFrame("Frame",nil,scroll)
        content:SetWidth(math.max(40,(width or 190)-10))
        content:SetHeight(math.max(1,#opts*25))
        scroll:SetScrollChild(content)
        scroll:SetScript("OnMouseWheel",function(self,delta)
            local maxScroll=math.max(0,content:GetHeight()-self:GetHeight())
            self:SetVerticalScroll(Clamp(self:GetVerticalScroll()-(delta*50),0,maxScroll))
        end)
        for i,opt in ipairs(opts) do
            local row=Button(content,opt.label,math.max(40,(width or 190)-12),23)
            row:SetPoint("TOPLEFT",0,-((i-1)*25))
            row:SetFrameLevel(popup:GetFrameLevel()+5)
            row.label:SetJustifyH("LEFT")
            row.label:ClearAllPoints();row.label:SetPoint("LEFT",8,0);row.label:SetPoint("RIGHT",-6,0)
            row:SetScript("OnClick",function()
                ClosePopup()
                if onSelect then
                    local display=onSelect(opt.value,opt.label)
                    b.label:SetText(tostring(display or opt.label))
                else
                    b.label:SetText(opt.label)
                end
            end)
        end
        popup:Show();popup:Raise()
    end
    b:SetScript("OnClick",function()
        if b._popup and b._popup:IsShown() then ClosePopup() else OpenPopup() end
    end)
    return b
end

local function PromptText(title,label,initial,onSave)
    local p=Palette();local f=CreateFrame("Frame",nil,UIParent,"BackdropTemplate");f:SetSize(420,180);f:SetPoint("CENTER");if RPAL.ApplyUILayer then RPAL.ApplyUILayer(f,"POPOVER")else f:SetFrameStrata("TOOLTIP");f:SetFrameLevel(12000)end;Backdrop(f,p.panel,p.gold);f:EnableMouse(true)
    local t=Font(f,title,15,p.gold);t:SetPoint("TOPLEFT",18,-16);local l=Font(f,label,9,p.muted);l:SetPoint("TOPLEFT",18,-48);local e=Edit(f,384,34,false);e:SetPoint("TOPLEFT",18,-68);e:SetText(initial or"");e:SetFocus();e:HighlightText()
    local cancel=Button(f,"CANCEL",88,28);cancel:SetPoint("BOTTOMLEFT",18,16);cancel:SetScript("OnClick",function()f:Hide()end);local save=Button(f,"SAVE",88,28);save:SetPoint("BOTTOMRIGHT",-18,16);save:SetScript("OnClick",function()local v=Trim(e:GetText());if onSave and onSave(v)==false then return end;f:Hide()end)
    return f,e
end
function RPAL.PromptCustomPermanentChange()
    PromptText("CUSTOM PERMANENT CHANGE","Name","",function(v)
        if v==""then return false end
        local d=DB();for _,n in ipairs(d.customAlterations)do if n:lower()==v:lower()then Toast("That permanent change already exists.");return false end end
        d.customAlterations[#d.customAlterations+1]=v:sub(1,40)
        Toast("Permanent Change preset added.")
        if RPAL.ResetInjuryUI then RPAL.ResetInjuryUI() end
        local reopen=function()if RPAL.OpenInjurySystem then RPAL.OpenInjurySystem()end end
        if C_Timer and C_Timer.After then C_Timer.After(0,reopen)else reopen()end
        return true
    end)
end

-- ---------- history / Health window chrome ----------
local HISTORY_FILTERS={
    {id="all",label="ALL"},{id="injury",label="INJURIES"},{id="care",label="CARE"},{id="notes",label="NOTES"},{id="changes",label="CHANGES"},
}
local function HistoryKindMeta(kind,p)
    kind=tostring(kind or "update");p=p or Palette()
    local red=p.red or HEALTH_PALETTE_DEFAULTS.red
    local heal=p.heal or HEALTH_PALETTE_DEFAULTS.heal
    local alteration=p.alteration or HEALTH_PALETTE_DEFAULTS.alteration
    local gold=p.gold or HEALTH_PALETTE_DEFAULTS.gold
    local muted=p.muted or HEALTH_PALETTE_DEFAULTS.muted
    local m={
        ["added"]={"injury","INJURY ADDED",red},["dm-injury"]={"injury","DM INJURY ACCEPTED",red},
        ["removed"]={"injury","RECORD REMOVED",muted},["severity"]={"injury","SEVERITY CHANGED",red},
        ["region"]={"injury","BODY REGION CHANGED",gold},["symptoms"]={"injury","SYMPTOMS UPDATED",gold},
        ["recovery"]={"care","RECOVERY UPDATED",heal},["treatment"]={"care","TREATMENT ACCEPTED",heal},
        ["stabilized"]={"care","STABILIZED",heal},["medical-note"]={"notes","MEDICAL NOTE",gold},
        ["note-privacy"]={"notes","NOTE VISIBILITY",muted},["aftermath"]={"changes","AFTERMATH",alteration},
        ["link"]={"changes","RECORD LINKED",alteration},["privacy"]={"changes","VISIBILITY CHANGED",muted},
        ["anatomy"]={"changes","ANATOMY PRESET",alteration},["session-snapshot"]={"changes","SESSION HEALTH SUMMARY",gold},
    }
    local v=m[kind] or {"changes",string.upper(kind:gsub("%-"," ")),gold}
    return v[1],v[2],v[3]
end
local function HistoryTransition(e)
    local from,to=Trim(e and e.from),Trim(e and e.to)
    if from~="" and to~="" then return from.."  →  "..to end
    if to~="" then return to end
    if from~="" then return from end
    return ""
end
local function HistoryDateKey(e)return date("%Y-%m-%d",tonumber(e and e.at)or time())end
local function HistoryDateLabel(e)return string.upper(date("%d %b %Y",tonumber(e and e.at)or time()))end
local function HistoryTimeLabel(e)return date("%H:%M",tonumber(e and e.at)or time())end
local function HistoryMatches(e,filter,p)
    if not filter or filter=="all" then return true end
    local category=HistoryKindMeta(e and e.kind,p)
    return category==filter
end
local function HistorySummary(history,p)
    local total,care,notes,sessions=0,0,0,0
    local lastAt=nil
    for _,e in ipairs(history or{})do
        total=total+1
        local category=HistoryKindMeta(e.kind,p)
        if category=="care"then care=care+1 elseif category=="notes"then notes=notes+1 end
        if e.kind=="session-snapshot"then sessions=sessions+1 end
        local at=tonumber(e.at);if at and(not lastAt or at>lastAt)then lastAt=at end
    end
    local last=lastAt and date("%d %b %Y • %H:%M",lastAt)or"NO HISTORY YET"
    return total,care,notes,sessions,string.upper(last)
end
local function HistoryNoteHeight(e)
    local n=#Trim(e and e.note)
    if n==0 then return 0 end
    if n<=90 then return 32 elseif n<=170 then return 48 else return 64 end
end
local function HistoryCardHeight(e,editable)
    local h=72
    if HistoryTransition(e)~=""then h=h+22 end
    if Trim(e and e.actor)~=""then h=h+16 end
    h=h+HistoryNoteHeight(e)
    if Trim(e and e.sceneTitle)~=""then h=h+18 end
    if editable then h=h+25 end
    return h
end
local function BuildHistoryEventCard(parent,e,width,y,p,accent,opts)
    opts=opts or{}
    local category,kindLabel,kindColor=HistoryKindMeta(e.kind,p)
    local h=HistoryCardHeight(e,opts.editable)
    local r=CreateFrame("Frame",nil,parent,"BackdropTemplate");r:SetSize(width,h);r:SetPoint("TOPLEFT",46,y);Backdrop(r,p.panel2,p.border)
    local stripe=r:CreateTexture(nil,"ARTWORK");stripe:SetColorTexture(kindColor[1],kindColor[2],kindColor[3],.95);stripe:SetPoint("TOPLEFT",0,-1);stripe:SetPoint("BOTTOMLEFT",0,1);stripe:SetWidth(3)
    local headerRule=r:CreateTexture(nil,"BACKGROUND");headerRule:SetColorTexture(kindColor[1],kindColor[2],kindColor[3],.18);headerRule:SetPoint("TOPLEFT",10,-24);headerRule:SetPoint("TOPRIGHT",-10,-24);headerRule:SetHeight(1)
    local dot=CreateFrame("Frame",nil,parent,"BackdropTemplate");dot:SetSize(9,9);dot:SetPoint("CENTER",parent,"TOPLEFT",23,y-18);Backdrop(dot,kindColor,kindColor)
    local arm=parent:CreateTexture(nil,"BACKGROUND");arm:SetColorTexture(kindColor[1],kindColor[2],kindColor[3],.55);arm:SetSize(20,1);arm:SetPoint("LEFT",dot,"RIGHT",1,0)
    local kind=Font(r,kindLabel,7,kindColor);kind:SetPoint("TOPLEFT",12,-10)
    local tm=Font(r,HistoryTimeLabel(e),7,p.muted);tm:SetPoint("TOPRIGHT",-12,-10)
    local label=Trim(e.label);if label==""then label="Health Record"end
    if e.kind=="session-snapshot" and Trim(e.sceneTitle)~=""then label=e.sceneTitle end
    local title=Font(r,label,10.5,p.pale);title:SetPoint("TOPLEFT",12,-29);title:SetPoint("RIGHT",-90,-29);title:SetJustifyH("LEFT")
    local cy=-50
    local trans=HistoryTransition(e)
    if trans~=""then
        local tr=CreateFrame("Frame",nil,r,"BackdropTemplate");tr:SetPoint("TOPLEFT",12,cy);tr:SetPoint("TOPRIGHT",-12,cy);tr:SetHeight(22);Backdrop(tr,{p.panel[1],p.panel[2],p.panel[3],.96},p.border)
        local tt=Font(tr,trans,8.2,kindColor);tt:SetPoint("LEFT",8,0);tt:SetPoint("RIGHT",-8,0);tt:SetJustifyH("LEFT");cy=cy-28
    end
    if Trim(e.actor)~=""then local a=Font(r,"BY  "..string.upper(Trim(e.actor)),7,p.muted);a:SetPoint("TOPLEFT",12,cy);cy=cy-16 end
    if Trim(e.note)~=""then
        local nh=HistoryNoteHeight(e);local n=Font(r,Trim(e.note),8.2,p.pale);n:SetPoint("TOPLEFT",12,cy);n:SetPoint("RIGHT",-12,cy);n:SetHeight(nh);n:SetJustifyH("LEFT");n:SetJustifyV("TOP");n:SetWordWrap(true);cy=cy-nh
    end
    if Trim(e.sceneTitle)~=""then
        local s=Font(r,"SESSION  •  "..tostring(e.sceneTitle),7,p.muted);s:SetPoint("BOTTOMLEFT",12,opts.editable and 32 or 8);s:SetPoint("RIGHT",-12,opts.editable and 32 or 8);s:SetJustifyH("LEFT")
    end
    if opts.editable then
        local edit=Button(r,"EDIT",54,19);edit:SetPoint("BOTTOMRIGHT",-74,7);edit:SetScript("OnClick",function()if RPAL.OpenHealthHistoryEditor then RPAL.OpenHealthHistoryEditor(e.id)end end)
        local del=Button(r,"DELETE",64,19);del:SetPoint("BOTTOMRIGHT",-6,7);del:SetScript("OnClick",function(self)
            if self._confirm then
                if RPAL.DeleteHealthHistoryEntry then RPAL.DeleteHealthHistoryEntry(e.id)end
                return
            end
            self._confirm=true;self.label:SetText("CONFIRM")
            if C_Timer and C_Timer.After then C_Timer.After(3,function()if self and self.label and self._confirm then self._confirm=nil;self.label:SetText("DELETE")end end)end
        end)
    end
    return r,dot,arm,h,category
end
local function BuildHistoryDateHeader(parent,group,y,p,accent,collapsed,onToggle)
    local h=CreateFrame("Button",nil,parent);h:SetHeight(22);h:SetPoint("TOPLEFT",4,y+4);h:SetPoint("RIGHT",parent,-8,y+4);h:EnableMouse(true);h:RegisterForClicks("LeftButtonUp")
    local marker=CreateFrame("Frame",nil,h,"BackdropTemplate");marker:SetSize(11,11);marker:SetPoint("LEFT",13,0);Backdrop(marker,p.panel2,accent)
    local arrow=Font(h,collapsed and "+" or "−",10,accent);arrow:SetPoint("LEFT",31,0)
    local label=Font(h,group.label,8,accent);label:SetPoint("LEFT",45,0)
    local count=Font(h,tostring(#group.events).." EVENT"..(#group.events==1 and""or"S"),7,p.muted);count:SetPoint("LEFT",label,"RIGHT",10,0)
    local rule=h:CreateTexture(nil,"BACKGROUND");rule:SetColorTexture(p.border[1],p.border[2],p.border[3],.65);rule:SetHeight(1);rule:SetPoint("LEFT",count,"RIGHT",12,0);rule:SetPoint("RIGHT",h,-4,0)
    h:SetScript("OnEnter",function()label:SetTextColor(accent[1],accent[2],accent[3],1);arrow:SetTextColor(1,1,1,1)end)
    h:SetScript("OnLeave",function()label:SetTextColor(accent[1],accent[2],accent[3],1);arrow:SetTextColor(accent[1],accent[2],accent[3],1)end)
    h:SetScript("OnClick",function()if onToggle then onToggle(group.key)end end)
    return h
end
local function GroupHistory(history,filter,p,limit)
    local groups,byKey={},{}
    local count=0
    for _,e in ipairs(history or{})do
        if HistoryMatches(e,filter,p)then
            local key=HistoryDateKey(e);local g=byKey[key]
            if not g then g={key=key,label=HistoryDateLabel(e),events={}};byKey[key]=g;groups[#groups+1]=g end
            g.events[#g.events+1]=e;count=count+1
            if limit and count>=limit then break end
        end
    end
    return groups,count
end
local function UpdateHistoryFilterButtons()
    local ui=RPAL.InjuryUI;if not ui or not ui.historyFilterButtons then return end
    local current=H2.historyFilter or"all"
    for id,b in pairs(ui.historyFilterButtons)do b:SetAlpha(id==current and 1 or .52)end
end
local function RefreshHistoryPanel()
    local ui=RPAL.InjuryUI;if not ui or not ui.historyContent then return end
    local c=ui.historyContent
    for _,x in ipairs(c._rows or{})do if x.Hide then x:Hide()end;if x.SetParent then x:SetParent(nil)end end;c._rows={}
    for _,x in ipairs(c._textures or{})do if x.Hide then x:Hide()end end;c._textures={}
    local p=Palette();local d=DB();local history=d.history or{};local filter=H2.historyFilter or"all"
    local total,care,notes,sessions,last=HistorySummary(history,p)
    if ui.historySummary then ui.historySummary:SetText(string.format("%d EVENTS    •    %d CARE UPDATES    •    %d NOTES    •    %d SESSION SUMMARIES    •    LAST CHANGE %s",total,care,notes,sessions,last))end
    UpdateHistoryFilterButtons()
    local groups,shown=GroupHistory(history,filter,p,120)
    if shown==0 then
        if c._timelineLine then c._timelineLine:Hide()end
        local empty=Font(c,filter=="all"and"No medical history yet. Health changes will appear here as a readable timeline."or"No medical history matches this filter.",10,p.muted);empty:SetPoint("TOPLEFT",20,-20);empty:SetWidth(math.max(320,c:GetWidth()-40));empty:SetJustifyH("LEFT");c._rows[#c._rows+1]=empty;c:SetHeight(90);return
    end
    if not c._timelineLine then c._timelineLine=c:CreateTexture(nil,"BACKGROUND");c._timelineLine:SetWidth(1)end
    c._timelineLine:SetColorTexture(p.border[1],p.border[2],p.border[3],.8);c._timelineLine:ClearAllPoints();c._timelineLine:SetPoint("TOPLEFT",23,-34);c._timelineLine:Show()
    local y=-8
    for gi,group in ipairs(groups)do
        if gi>1 then y=y-8 end
        local collapsed=H2.collapsedDays[group.key] and true or false
        local head=BuildHistoryDateHeader(c,group,y,p,p.gold,collapsed,function(key)H2.collapsedDays[key]=not H2.collapsedDays[key];RefreshHistoryPanel()end);c._rows[#c._rows+1]=head
        y=y-28
        if not collapsed then
            for _,e in ipairs(group.events)do
                local r,dot,arm,h=BuildHistoryEventCard(c,e,math.max(300,c:GetWidth()-58),y,p,p.gold,{editable=true})
                c._rows[#c._rows+1]=r;c._rows[#c._rows+1]=dot;c._textures[#c._textures+1]=arm
                y=y-h-10
            end
        end
    end
    c:SetHeight(math.max(1,-y+18));c._timelineLine:SetPoint("BOTTOMLEFT",23,12)
end
function RPAL.SetHealthViewMode(mode)
    H2.mode=(mode=="history")and"history"or"active";local ui=RPAL.InjuryUI;if not ui or not ui.frame then return end;local history=H2.mode=="history"
    if ui.left then ui.left:SetShown(not history)end;if ui.bodyPanel then ui.bodyPanel:SetShown(not history)end;if ui.rightScroll then ui.rightScroll:SetShown(not history)end;if ui.historyFrame then ui.historyFrame:SetShown(history)end
    if ui.activeTab then ui.activeTab:SetAlpha(history and .55 or 1)end;if ui.historyTab then ui.historyTab:SetAlpha(history and 1 or .55)end;if history then RefreshHistoryPanel()end
end
local function EnsureHealthChrome()
    local ui=RPAL.InjuryUI;local f=ui and ui.frame;if not f or ui.health2Chrome then return end;ui.health2Chrome=true;local p=Palette()
    ui.activeTab=Button(f,"ACTIVE",76,24);ui.activeTab:SetPoint("TOPRIGHT",-278,-18);ui.activeTab:SetScript("OnClick",function()RPAL.SetHealthViewMode("active")end)
    ui.historyTab=Button(f,"HISTORY",76,24);ui.historyTab:SetPoint("LEFT",ui.activeTab,"RIGHT",6,0);ui.historyTab:SetScript("OnClick",function()RPAL.SetHealthViewMode("history")end)
    if ui.bodyPanel then
        if ui.bodyHint then ui.bodyHint:Hide() end;if ui.bodyRule then ui.bodyRule:Hide() end
        ui.anatomyButton=Button(ui.bodyPanel,"BODY: HUMANOID",154,22);ui.anatomyButton:SetPoint("TOPLEFT",10,-5);ui.anatomyButton:SetScript("OnClick",RPAL.CycleAnatomyPreset)
        ui.traitButtons={};local x=170;for _,k in ipairs({"tail","wings","horns","hooves"})do local b=Button(ui.bodyPanel,string.upper(k),66,22);b:SetPoint("TOPLEFT",x,-5);b:SetScript("OnClick",function()RPAL.ToggleAnatomyTrait(k)end);ui.traitButtons[k]=b;x=x+72 end
    end
    ui.historyFrame=CreateFrame("Frame",nil,f,"BackdropTemplate");ui.historyFrame:SetPoint("TOPLEFT",14,-76);ui.historyFrame:SetPoint("BOTTOMRIGHT",-28,18);Backdrop(ui.historyFrame,p.panel2,p.border);ui.historyFrame:Hide()
    local title=Font(ui.historyFrame,"MEDICAL HISTORY",18,p.gold);title:SetPoint("TOPLEFT",18,-14)
    ui.wipeHistoryButton=Button(ui.historyFrame,"WIPE ALL HISTORY",126,24);ui.wipeHistoryButton:SetPoint("TOPRIGHT",-18,-12)
    ui.wipeHistoryButton:SetScript("OnClick",function(self)
        local d=DB();if not d then return end
        if #(d.history or{})==0 then Toast("Medical History is already empty.");return end
        if not self._confirmWipe then
            self._confirmWipe=true;self.label:SetText("CONFIRM WIPE");self:SetBackdropBorderColor(.78,.2,.16,1)
            Toast("Click CONFIRM WIPE to permanently clear this character's Medical History.")
            if C_Timer and C_Timer.After then C_Timer.After(4,function()
                if self and self._confirmWipe then self._confirmWipe=nil;if self.label then self.label:SetText("WIPE ALL HISTORY")end;local pp=Palette();if self.SetBackdropBorderColor then self:SetBackdropBorderColor(unpack(pp.border))end end
            end)end
            return
        end
        self._confirmWipe=nil;d.history={};H2.collapsedDays={};self.label:SetText("WIPE ALL HISTORY");self:SetBackdropBorderColor(unpack(Palette().border));RefreshHistoryPanel();Toast("Medical History wiped. Active Health records were not changed.")
    end)
    local sub=Font(ui.historyFrame,"A chronological record of injuries, treatment, recovery and lasting changes.",9,p.muted);sub:SetPoint("TOPLEFT",18,-40)
    ui.historySummary=Font(ui.historyFrame,"",7.5,p.muted);ui.historySummary:SetPoint("TOPLEFT",18,-64);ui.historySummary:SetPoint("RIGHT",-18,-64);ui.historySummary:SetJustifyH("LEFT")
    local rule=ui.historyFrame:CreateTexture(nil,"BACKGROUND");rule:SetColorTexture(p.border[1],p.border[2],p.border[3],.7);rule:SetPoint("TOPLEFT",18,-84);rule:SetPoint("TOPRIGHT",-18,-84);rule:SetHeight(1)
    ui.historyFilterButtons={};local bx=18
    for _,spec in ipairs(HISTORY_FILTERS)do
        local b=Button(ui.historyFrame,spec.label,spec.id=="all"and 54 or 78,22);b:SetPoint("TOPLEFT",bx,-95);b:SetScript("OnClick",function()H2.historyFilter=spec.id;RefreshHistoryPanel()end);ui.historyFilterButtons[spec.id]=b;bx=bx+(spec.id=="all"and 60 or 84)
    end
    local hint=Font(ui.historyFrame,"Newest first  •  Click a day to collapse  •  Edit/delete affects this character's history",7,p.muted);hint:SetPoint("TOPRIGHT",-20,-101)
    local scroll=CreateFrame("ScrollFrame",nil,ui.historyFrame,"UIPanelScrollFrameTemplate");scroll:SetPoint("TOPLEFT",14,-128);scroll:SetPoint("BOTTOMRIGHT",-28,14);local content=CreateFrame("Frame",nil,scroll);content:SetWidth(math.max(600,(ui.historyFrame:GetWidth()or 900)-58));content:SetHeight(1);scroll:SetScrollChild(content);content._rows={};content._textures={};ui.historyContent=content
    if RPAL.SkinScrollFrame then RPAL.SkinScrollFrame(scroll) end
    RPAL.SetHealthViewMode(H2.mode);RPAL.RefreshHealthAnatomyVisual()
end
function RPAL.RefreshHealth2UI()
    -- Health2 is a child refresh of InjuryUI. Calling RefreshInjuryUI() back from
    -- here created an unbounded RefreshInjuryUI -> RefreshHealth2UI recursion
    -- whenever the Health window was visible. On some machines this presented
    -- as a multi-second freeze; on larger Health datasets it could lock WoW.
    EnsureHealthChrome()
    if H2.mode=="history" then RefreshHistoryPanel() end
    RPAL.RefreshHealthAnatomyVisual()
end

-- ---------- advanced record editor ----------
local recordModal,recordModalRoot
local function AddMedicalNote(item,author,text,isPublic)
    text=Trim(text);if text==""then return end;item.medicalNotes=item.medicalNotes or{};table.insert(item.medicalNotes,1,{id="mn"..time()..math.random(10,99),at=time(),author=Trim(author)~=""and Trim(author)or(UnitName("player")or"Unknown"),text=text:sub(1,500),public=isPublic~=false});while #item.medicalNotes>12 do table.remove(item.medicalNotes)end;RPAL.HealthAddHistory("medical-note",item,"","Added",author,text:sub(1,100))
end
local function FindItem(id)local d=DB();return d and d.items and d.items[tostring(id)]end
local function SaveAndRefresh()if RPAL.RefreshInjuryUI then RPAL.RefreshInjuryUI()end;if recordModal and recordModal:IsShown()and recordModal.itemID then RPAL.OpenHealthRecordEditor(recordModal.itemID)end end
local function RemoveDirect(item)
    local d=DB();if not item then return end;local id=tostring(item.id);for gid,g in pairs(d.groups or{})do local keep={};for _,x in ipairs(g.itemIds or{})do if tostring(x)~=id then keep[#keep+1]=x end end;g.itemIds=keep;if #keep==0 then d.groups[gid]=nil end end;d.items[id]=nil
end
local function ConvertAftermath(item,mode)
    if not item then return end;local old=ItemLabel(item)
    if mode=="remove"then RPAL.HealthAddHistory("aftermath",item,"Healed","Removed",nil,"Injury resolved without a lasting change");RemoveDirect(item)
    elseif mode=="scar"then item.category="alteration";item.type="Scar";item.customName="Scar — "..old;item.stage=nil;item.severity=nil;item.originLabel=old;RPAL.HealthAddHistory("aftermath",item,"Healed","Scar",nil,"Converted to a lasting scar")
    else item.category="alteration";item.type="Permanent Change";item.customName=old.." Aftermath";item.stage=nil;item.severity=nil;item.originLabel=old;RPAL.HealthAddHistory("aftermath",item,"Healed","Permanent Change",nil,"Converted to a lasting change")end
    H2.shadow=Shadow();if recordModalRoot then recordModalRoot:Hide()elseif recordModal then recordModal:Hide()end;if RPAL.RefreshInjuryUI then RPAL.RefreshInjuryUI()end
end
local function CycleValue(item,key,values,historyKind)
    local old=item[key]or values[1];item[key]=Cycle(values,old);RPAL.HealthAddHistory(historyKind or key,item,old,item[key]);return item[key]
end
local function RegionCycle(item)
    local current=Trim(item.regionOverride);local display=current~=""and current or"Auto";local nextv=Cycle(REGIONS,display);item.regionOverride=(nextv=="Auto")and nil or nextv;return RPAL.HealthGetRegion(item)
end
local function LinkSourceCycle(item)
    local d=DB();local list={{id=nil,label="None"}};for id,x in pairs(d.items or{})do if x.category=="injury"and tostring(id)~=tostring(item.id)then list[#list+1]={id=id,label=ItemLabel(x).." — "..RPAL.HealthGetRegion(x)}end end;table.sort(list,function(a,b)return a.label<b.label end);local idx=1;for i,v in ipairs(list)do if tostring(v.id or"")==tostring(item.originId or"")then idx=i break end end;idx=idx%#list+1;item.originId=list[idx].id;item.originLabel=list[idx].id and list[idx].label or nil;return list[idx].label
end
function RPAL.OpenHealthRecordEditor(id)
    local item=FindItem(id);if not item then return end;local p=Palette()
    if recordModalRoot then recordModalRoot:Hide();recordModalRoot:SetParent(nil);recordModalRoot=nil elseif recordModal then recordModal:Hide();recordModal:SetParent(nil)end
    -- Dedicated modal root: this owns the entire mouse stack while the record
    -- editor is open.  The old editor was drawn at a high strata but could
    -- still lose input to another top-level ODYN/UIParent frame underneath,
    -- leaving visible buttons that never received OnClick.
    local root=CreateFrame("Frame",nil,UIParent);recordModalRoot=root;root:SetAllPoints(UIParent);if RPAL.ApplyUILayer then RPAL.ApplyUILayer(root,"MODAL",100)else root:SetFrameStrata("FULLSCREEN_DIALOG");root:SetFrameLevel(8100);root:SetToplevel(true)end;root:EnableMouse(false);root:Show();root:Raise()
    -- The fullscreen root itself must never own mouse input.  A dedicated
    -- blocker sits below the visible modal so background UI is blocked without
    -- creating an invisible click-eating layer over the controls.
    local blocker=CreateFrame("Button",nil,root);blocker:SetAllPoints(root);blocker:SetFrameLevel(root:GetFrameLevel()+1);blocker:EnableMouse(true);blocker:RegisterForClicks("LeftButtonUp","RightButtonUp");blocker:SetScript("OnClick",function()end);blocker:Show()
    local f=CreateFrame("Frame",nil,root,"BackdropTemplate");recordModal=f;f.itemID=tostring(id);f:SetSize(670,700);f:SetPoint("CENTER");f:SetFrameLevel(root:GetFrameLevel()+30);f:SetToplevel(true);Backdrop(f,p.panel,p.gold);f:EnableMouse(true);f:Show();f:Raise()
    local function CloseRecordModal()
        if recordModalRoot then recordModalRoot:Hide();recordModalRoot:SetParent(nil);recordModalRoot=nil end
        recordModal=nil
    end
    local function Lift(control,delta)
        if not control then return control end
        control:SetFrameLevel(f:GetFrameLevel()+(delta or 4));control:EnableMouse(true);return control
    end
    local title=Font(f,ItemLabel(item),18,p.gold);title:SetPoint("TOPLEFT",20,-18);local sub=Font(f,string.upper(item.category or"record").." • "..RPAL.HealthGetRegion(item),9,p.muted);sub:SetPoint("TOPLEFT",21,-44);local close=Lift(Button(f,"CLOSE",72,26));close:SetPoint("TOPRIGHT",-16,-14);close:SetScript("OnClick",CloseRecordModal)
    local y=-80
    local function RowDropdown(label,value,width,options,onSelect)
        local l=Font(f,label,8,p.muted);l:SetPoint("TOPLEFT",22,y)
        local b=Dropdown(f,value,width or 250,options,function(v,display)
            local shown=onSelect and onSelect(v,display) or display
            return shown or display
        end)
        Lift(b,6);b:SetPoint("TOPLEFT",150,y+7);y=y-42;return b
    end
    if item.category=="injury"then
        RowDropdown("RECOVERY STAGE",string.upper(item.stage or"fresh"),250,(function()local o={};for _,v in ipairs(STAGES)do o[#o+1]={value=v,label=string.upper(v)}end;return o end)(),function(v)
            local old=item.stage or"fresh";item.stage=v;RPAL.HealthAddHistory("recovery",item,old,v);return string.upper(v)
        end)
        RowDropdown("SEVERITY",string.upper(item.severity or"moderate"),250,(function()local o={};for _,v in ipairs(SEVERITIES)do o[#o+1]={value=v,label=string.upper(v)}end;return o end)(),function(v)
            local old=item.severity or"moderate";item.severity=v;RPAL.HealthAddHistory("severity",item,old,v);return string.upper(v)
        end)
    end
    local regionAutoLabel="AUTO • "..RPAL.HealthGetRegion(item)
    local regionOptions={{value="Auto",label=regionAutoLabel}}
    for _,v in ipairs(REGIONS)do if v~="Auto"then regionOptions[#regionOptions+1]={value=v,label=v}end end
    RowDropdown("BODY REGION",Trim(item.regionOverride)~="" and item.regionOverride or regionAutoLabel,280,regionOptions,function(v)
        local old=RPAL.HealthGetRegion(item)
        item.regionOverride=(v=="Auto")and nil or v
        local actual=RPAL.HealthGetRegion(item);RPAL.HealthAddHistory("region",item,old,actual)
        return item.regionOverride or ("AUTO • "..actual)
    end)
    local privacyOptions={}
    for _,v in ipairs(PRIVACY)do privacyOptions[#privacyOptions+1]={value=v,label=string.upper(v)}end
    RowDropdown("PRIVACY",string.upper(item.privacy or"public"),250,privacyOptions,function(v)
        local old=item.privacy or"public";item.privacy=v;RPAL.HealthAddHistory("privacy",item,old,v);return string.upper(v)
    end)
    if item.category=="alteration"then
        local function LinkOptions()
            local d=DB();local list={{value="",label="None"}}
            for linkID,x in pairs(d.items or{})do
                if x.category=="injury"and tostring(linkID)~=tostring(item.id)then list[#list+1]={value=tostring(linkID),label=ItemLabel(x).." — "..RPAL.HealthGetRegion(x)}end
            end
            table.sort(list,function(a,b)if a.value==""then return true elseif b.value==""then return false end;return a.label<b.label end)
            return list
        end
        RowDropdown("RESULT OF",item.originLabel or"None",310,LinkOptions,function(v,label)
            local old=item.originLabel or"None";item.originId=(v~="")and v or nil;item.originLabel=(v~="")and label or nil;RPAL.HealthAddHistory("link",item,old,label);return label
        end)
    end
    local symLabel=Font(f,"SYMPTOMS / EFFECTS  •  COMMA SEPARATED",8,p.muted);symLabel:SetPoint("TOPLEFT",22,y);y=y-18;local symptoms=Lift(Edit(f,626,42,true));symptoms:SetPoint("TOPLEFT",22,y);symptoms:SetText(SymptomsText(item));y=y-52;local saveEffects=Lift(Button(f,"SAVE EFFECTS",116,24));saveEffects:SetPoint("TOPRIGHT",-22,y+5);saveEffects:SetScript("OnClick",function()local old=SymptomsText(item);item.symptoms=ParseSymptoms(symptoms:GetText());RPAL.HealthAddHistory("symptoms",item,old,SymptomsText(item));Toast("Symptoms updated.")end);y=y-30
    local med=Font(f,"MEDICAL NOTES",10,p.gold);med:SetPoint("TOPLEFT",22,y);y=y-22
    local shownNotes=0
    for i,n in ipairs(item.medicalNotes or{})do
        if i<=4 then
            shownNotes=shownNotes+1
            local line=Font(f,date("%m/%d",n.at or time()).." • "..tostring(n.author or"Unknown").." • "..tostring(n.text or""),7.5,p.pale)
            line:SetPoint("TOPLEFT",22,y);line:SetWidth(510);line:SetWordWrap(false);line:SetJustifyH("LEFT")
            local vis=Lift(Button(f,n.public==false and"PRIVATE"or"PUBLIC",88,20));vis:SetPoint("TOPRIGHT",-22,y+4)
            vis:SetScript("OnClick",function()
                n.public=not (n.public~=false)
                RPAL.HealthAddHistory("note-privacy",item,"",n.public and"Public"or"Private",UnitName("player"),tostring(n.text or""):sub(1,70))
                RPAL.OpenHealthRecordEditor(id)
            end)
            y=y-24
        end
    end
    if shownNotes==0 then local empty=Font(f,"No medical notes yet.",8,p.muted);empty:SetPoint("TOPLEFT",22,y);y=y-28 end
    local noteEdit=Lift(Edit(f,480,48,true));noteEdit:SetPoint("TOPLEFT",22,y);local public=true;local pub=Lift(Button(f,"PUBLIC",112,24));pub:SetPoint("TOPRIGHT",-22,y);pub:SetScript("OnClick",function()public=not public;pub.label:SetText(public and"PUBLIC"or"PRIVATE")end);local add=Lift(Button(f,"ADD NOTE",112,24));add:SetPoint("TOPRIGHT",-22,y-29);add:SetScript("OnClick",function()AddMedicalNote(item,UnitName("player"),noteEdit:GetText(),public);RPAL.OpenHealthRecordEditor(id)end);y=y-62
    if item.category=="injury"and item.stage=="healed"then
        local a=Font(f,"AFTERMATH",9,p.gold);a:SetPoint("BOTTOMLEFT",22,62);local remove=Lift(Button(f,"REMOVE",110,28));remove:SetPoint("BOTTOMLEFT",22,22);remove:SetScript("OnClick",function()ConvertAftermath(item,"remove")end);local scar=Lift(Button(f,"CONVERT TO SCAR",142,28));scar:SetPoint("LEFT",remove,"RIGHT",8,0);scar:SetScript("OnClick",function()ConvertAftermath(item,"scar")end);local perm=Lift(Button(f,"PERMANENT CHANGE",166,28));perm:SetPoint("LEFT",scar,"RIGHT",8,0);perm:SetScript("OnClick",function()ConvertAftermath(item,"permanent")end)
    else local done=Lift(Button(f,"DONE",90,28));done:SetPoint("BOTTOMRIGHT",-22,20);done:SetScript("OnClick",function()CloseRecordModal();if RPAL.RefreshInjuryUI then RPAL.RefreshInjuryUI()end end)end
    -- A few Blizzard/ODYN top-level frames raise themselves at the end of the
    -- same click that opened this editor. Re-assert the modal once after that
    -- click completes so both draw order and mouse order remain deterministic.
    if C_Timer and C_Timer.After then C_Timer.After(0,function()if recordModal==f and f:IsShown()then root:Raise();f:Raise()end end)end
end

-- ---------- Medical History edit/delete ----------
local historyModalRoot
local function CloseHistoryModal()
    if historyModalRoot then historyModalRoot:Hide();historyModalRoot:SetParent(nil);historyModalRoot=nil end
end
local function FindHistoryEntry(id)
    local d=DB();for index,e in ipairs(d and d.history or{})do if tostring(e.id or"")==tostring(id or"")then return e,index,d end end
end
function RPAL.DeleteHealthHistoryEntry(id)
    local e,index,d=FindHistoryEntry(id);if not e or not index then return false end
    table.remove(d.history,index);Toast("Medical History entry deleted.");RefreshHistoryPanel();return true
end
function RPAL.OpenHealthHistoryEditor(id)
    local e=FindHistoryEntry(id);if not e then return end;local p=Palette();CloseHistoryModal()
    local root=CreateFrame("Frame",nil,UIParent);historyModalRoot=root;root:SetAllPoints(UIParent);if RPAL.ApplyUILayer then RPAL.ApplyUILayer(root,"MODAL",200)else root:SetFrameStrata("FULLSCREEN_DIALOG");root:SetFrameLevel(8200);root:SetToplevel(true)end;root:EnableMouse(false);root:Show();root:Raise()
    local blocker=CreateFrame("Button",nil,root);blocker:SetAllPoints(root);blocker:SetFrameLevel(root:GetFrameLevel()+1);blocker:EnableMouse(true);blocker:RegisterForClicks("LeftButtonUp","RightButtonUp");blocker:SetScript("OnClick",function()end)
    local f=CreateFrame("Frame",nil,root,"BackdropTemplate");f:SetSize(680,560);f:SetPoint("CENTER");f:SetFrameLevel(root:GetFrameLevel()+30);f:SetToplevel(true);f:EnableMouse(true);Backdrop(f,p.panel,p.gold);f:Show();f:Raise()
    local title=Font(f,"EDIT MEDICAL HISTORY",17,p.gold);title:SetPoint("TOPLEFT",20,-18)
    local _,kindLabel=HistoryKindMeta(e.kind,p);local meta=Font(f,kindLabel.."  •  "..date("%d %b %Y  %H:%M",tonumber(e.at)or time()),8,p.muted);meta:SetPoint("TOPLEFT",21,-45)
    local close=Button(f,"CLOSE",72,26);close:SetPoint("TOPRIGHT",-16,-14);close:SetFrameLevel(f:GetFrameLevel()+5);close:SetScript("OnClick",CloseHistoryModal)
    local function L(text,y)local l=Font(f,text,8,p.muted);l:SetPoint("TOPLEFT",22,y);return l end
    L("TITLE",-82);local label=Edit(f,636,30,false);label:SetPoint("TOPLEFT",22,-99);label:SetText(e.label or"")
    L("FROM",-142);local from=Edit(f,300,30,false);from:SetPoint("TOPLEFT",22,-159);from:SetText(e.from or"")
    L("TO",-142);local to=Edit(f,300,30,false);to:SetPoint("TOPRIGHT",-22,-159);to:SetText(e.to or"")
    L("ACTOR / AUTHOR",-202);local actor=Edit(f,300,30,false);actor:SetPoint("TOPLEFT",22,-219);actor:SetText(e.actor or"")
    L("VISIBILITY",-202);local privacy=e.privacy or"odyn";local popts={{value="public",label="PUBLIC"},{value="odyn",label="ODYN USERS"},{value="private",label="PRIVATE"}}
    local priv=Dropdown(f,privacy=="private"and"PRIVATE"or(privacy=="public"and"PUBLIC"or"ODYN USERS"),300,popts,function(v,l)privacy=v;return l end);priv:SetPoint("TOPRIGHT",-22,-219);priv:SetFrameLevel(f:GetFrameLevel()+6)
    L("NOTE / DETAILS",-265);local note=Edit(f,636,150,true);note:SetPoint("TOPLEFT",22,-282);note:SetText(e.note or"");note:SetFrameLevel(f:GetFrameLevel()+5)
    local info=Font(f,"History edits change the narrative log only; they do not alter the current injury state.",7.5,p.muted);info:SetPoint("BOTTOMLEFT",22,60);info:SetWidth(500);info:SetWordWrap(true)
    local save=Button(f,"SAVE CHANGES",126,28);save:SetPoint("BOTTOMRIGHT",-22,20);save:SetFrameLevel(f:GetFrameLevel()+6);save:SetScript("OnClick",function()
        e.label=Trim(label:GetText())~=""and Trim(label:GetText())or"Health Record";e.from=Trim(from:GetText());e.to=Trim(to:GetText());e.actor=Trim(actor:GetText());e.note=Trim(note:GetText()):sub(1,700);e.privacy=privacy;e.editedAt=time()
        CloseHistoryModal();RefreshHistoryPanel();Toast("Medical History entry updated.")
    end)
    if C_Timer and C_Timer.After then C_Timer.After(0,function()if historyModalRoot==root and root:IsShown()then root:Raise();f:Raise()end end)end
end

-- ---------- public snapshot extension ----------
local baseSnapshot=RPAL.GetPublicInjurySnapshot
RPAL.GetPublicInjurySnapshot=function()
    local out=baseSnapshot and baseSnapshot()or{items={},groups={}};local d=DB();out.items=out.items or{};out.groups=out.groups or{};out.anatomy=Copy(d.anatomy or{});out.history={}
    local privateIDs={}
    for id,snap in pairs(out.items)do local src=d.items[id];if src and src.privacy=="private"then privateIDs[#privateIDs+1]=id else
        snap.stage=src and src.stage;snap.severity=src and src.severity;snap.stabilized=src and src.stabilized and true or nil;snap.region=src and RPAL.HealthGetRegion(src);snap.regionOverride=src and src.regionOverride;snap.privacy=src and src.privacy or"public";snap.symptoms=src and Copy(src.symptoms or{})or{};snap.originId=src and src.originId;snap.originLabel=src and src.originLabel;snap.medicalNotes={}
        for _,n in ipairs(src and src.medicalNotes or{})do if n.public~=false and #snap.medicalNotes<4 then snap.medicalNotes[#snap.medicalNotes+1]=Copy(n)end end
    end end
    for _,id in ipairs(privateIDs)do out.items[id]=nil end
    for gid,g in pairs(Copy(out.groups))do local ids={};for _,id in ipairs(g.itemIds or{})do if out.items[tostring(id)]then ids[#ids+1]=tostring(id)end end;if #ids==0 then out.groups[gid]=nil else out.groups[gid].itemIds=ids end end
    for _,e in ipairs(d.history or{})do
        local current=e.itemID and d.items[tostring(e.itemID)] or nil
        local hidden=(e.privacy=="private") or (current and current.privacy=="private")
        if not hidden and #out.history<60 then out.history[#out.history+1]=Copy(e)end
    end
    return out
end

-- ---------- Health network / treatment consent ----------
local function Hex(s)
    s=tostring(s or "");if s=="" then return "-" end
    local out={};for i=1,#s do out[#out+1]=string.format("%02X",string.byte(s,i))end;return table.concat(out)
end
local function Unhex(s)
    s=tostring(s or "");if s=="-" or s=="" then return "" end
    local out={};for i=1,#s-1,2 do local n=tonumber(s:sub(i,i+1),16);if n then out[#out+1]=string.char(n)end end;return table.concat(out)
end
local function Send(target,msg)
    if not target or target=="" or not C_ChatInfo or type(C_ChatInfo.SendAddonMessage)~="function" then return false end
    if RPAL.SendAddonMessageSafe then return RPAL.SendAddonMessageSafe(PREFIX,msg,"WHISPER",target,true) end
    local ok,result=pcall(C_ChatInfo.SendAddonMessage,PREFIX,msg,"WHISPER",target)
    return ok and (result==nil or result==true or tonumber(result)==0)
end
local function RequestID()return tostring(time())..tostring(math.random(100,999))end
local function LocalName()local n,r=UnitFullName("player");return r and r~=""and(n.."-"..r)or n end
local function InList(list,value)
    for _,v in ipairs(list or {})do if tostring(v):lower()==tostring(value or""):lower()then return v end end
    return nil
end
local REGION_PLACEMENT={
    ["Head"]={.50,.10,"front"},["Neck"]={.50,.20,"front"},["Chest"]={.50,.33,"front"},["Upper Back"]={.50,.33,"back"},
    ["Abdomen"]={.50,.50,"front"},["Lower Back"]={.50,.50,"back"},["Left Shoulder"]={.25,.31,"front"},["Right Shoulder"]={.75,.31,"front"},
    ["Left Upper Arm"]={.19,.42,"front"},["Right Upper Arm"]={.81,.42,"front"},["Left Forearm"]={.15,.55,"front"},["Right Forearm"]={.85,.55,"front"},
    ["Left Hand"]={.11,.68,"front"},["Right Hand"]={.89,.68,"front"},["Hips"]={.50,.64,"front"},["Left Thigh"]={.42,.75,"front"},
    ["Right Thigh"]={.58,.75,"front"},["Left Knee"]={.42,.84,"front"},["Right Knee"]={.58,.84,"front"},["Left Lower Leg"]={.42,.91,"front"},
    ["Right Lower Leg"]={.58,.91,"front"},["Left Foot"]={.42,.98,"front"},["Right Foot"]={.58,.98,"front"},
}
local function RegionPlacement(region)
    local p=REGION_PLACEMENT[tostring(region or"")]
    if p then return p[1],p[2],p[3] end
    return .5,.43,"front"
end
local function CreateHealthItem(kind,category,source,severity,region)
    local d=DB();d.nextID=(tonumber(d.nextID)or 0)+1
    local id="inj"..d.nextID
    local cleanCategory=category or"injury"
    local cleanSeverity=InList(SEVERITIES,severity)or"moderate"
    -- DM proposals may deliberately leave the anatomical location unspecified.
    -- Do not silently invent an "Auto" body region for another player's record.
    local cleanRegion=(tostring(region or""):lower()=="unspecified")and"Unspecified"or(InList(REGIONS,region)or"Unspecified")
    local x,y,side=RegionPlacement(cleanRegion)
    local item={id=id,type=kind,category=cleanCategory,x=x,y=y,side=side,scale=1,rotation=0,createdAt=time(),privacy="odyn",stage=(cleanCategory=="injury")and"fresh"or nil,severity=(cleanCategory=="injury")and cleanSeverity or nil,symptoms={},medicalNotes={},dmSource=source}
    if cleanRegion~="Auto"then item.regionOverride=cleanRegion end
    d.items[id]=item
    d.nextGroupID=(tonumber(d.nextGroupID)or 0)+1
    local gid="grp"..d.nextGroupID
    d.groups[gid]={id=gid,name=kind,itemIds={id},side=side,anchorX=x,anchorY=y};item.groupId=gid
    RPAL.HealthAddHistory(source and"dm-injury"or"added",item,"",kind,source,source and"DM-proposed injury accepted"or"")
    H2.shadow=Shadow();return item
end
local function ApplyIncoming(req,sender)
    local item=(req.itemID and req.itemID~="-") and FindItem(req.itemID) or nil
    if req.action=="TEMP"then
        -- TEMP is retained as the wire token for backwards compatibility, but
        -- DM-proposed injuries are permanent Health records once accepted.
        -- They are never auto-removed when a DM session/encounter ends.
        item=CreateHealthItem(req.kind~=""and req.kind or"Injury","injury",sender,req.severity,req.region)
    elseif not item then return false,"That Health record no longer exists." end
    if req.action=="TREAT"then
        local old=item.stage or "fresh"
        if old=="fresh" then item.stage="treated" end
        local detail=req.note~=""and req.note or"Treatment offered and accepted."
        AddMedicalNote(item,sender,detail,true)
        RPAL.HealthAddHistory("treatment",item,old,item.stage or old,sender,req.note)
    elseif req.action=="STABILIZE"then
        local oldState=item.stabilized and "Stabilized" or "Not stabilized"
        item.stabilized=true
        local detail=req.note~=""and req.note or"Injury stabilized."
        AddMedicalNote(item,sender,detail,true)
        RPAL.HealthAddHistory("stabilized",item,oldState,"Stabilized",sender,req.note)
    elseif req.action=="NOTE"then
        if Trim(req.note)=="" then return false,"Medical note cannot be empty." end
        AddMedicalNote(item,sender,req.note,true)
    elseif req.action=="TEMP"then
        local detail="DM-proposed injury accepted"
        if req.note~=""then detail=detail..": "..req.note end
        AddMedicalNote(item,sender,detail,true)
    end
    if RPAL.RefreshInjuryUI then RPAL.RefreshInjuryUI()end;return true
end
local function IncomingPrompt(req,sender)
    local p=Palette()
    -- Dedicated modal root.  A plain high-frame-level child of UIParent could
    -- remain visible while another ODYN top-level frame owned the mouse stack,
    -- producing the "frozen request" symptom with no Lua error.
    local root=CreateFrame("Frame",nil,UIParent)
    root:SetAllPoints(UIParent);if RPAL.ApplyUILayer then RPAL.ApplyUILayer(root,"MODAL",300)else root:SetFrameStrata("FULLSCREEN_DIALOG");root:SetFrameLevel(8300);root:SetToplevel(true)end;root:EnableMouse(false);root:Show();root:Raise()
    local blocker=CreateFrame("Button",nil,root);blocker:SetAllPoints(root);blocker:SetFrameLevel(root:GetFrameLevel()+1);blocker:EnableMouse(true);blocker:RegisterForClicks("LeftButtonUp","RightButtonUp");blocker:SetScript("OnClick",function()end);blocker:Show()
    local f=CreateFrame("Frame",nil,root,"BackdropTemplate")
    f:SetSize(480,230);f:SetPoint("CENTER");f:SetFrameLevel(root:GetFrameLevel()+30);f:SetToplevel(true);f:EnableMouse(true);Backdrop(f,p.panel,p.gold);f:Show();f:Raise()
    local function Close()
        if root then root:Hide();root:SetParent(nil);root=nil end
    end
    local t=Font(f,"HEALTH REQUEST",16,p.gold);t:SetPoint("TOPLEFT",18,-18)
    local action=({TREAT="offers treatment for",STABILIZE="wants to mark stabilized",NOTE="wants to add a medical note to",TEMP="DM proposes an injury:"})[req.action]or"requests a Health change"
    local item=FindItem(req.itemID)
    local requestRegion=tostring(req.region or"Unspecified")
    if requestRegion=="Auto" then requestRegion="Unspecified" end
    local recordText=req.action=="TEMP"and(req.kind.." • "..string.upper(req.severity or"moderate").." • "..requestRegion)or ItemLabel(item)
    local body=Font(f,tostring(sender).." "..action.."\n"..recordText..(req.note~=""and("\n\n“"..req.note.."”")or""),10,p.pale);body:SetPoint("TOPLEFT",18,-56);body:SetWidth(444);body:SetWordWrap(true)
    local no=Button(f,"DECLINE",100,30);no:SetPoint("BOTTOMLEFT",18,18);no:SetFrameLevel(f:GetFrameLevel()+10);no:SetScript("OnClick",function()Send(sender,"RES|"..req.id.."|NO");Close()end)
    local yes=Button(f,"ACCEPT",100,30);yes:SetPoint("BOTTOMRIGHT",-18,18);yes:SetFrameLevel(f:GetFrameLevel()+10);yes:SetScript("OnClick",function()
        local ok,err=ApplyIncoming(req,sender)
        Send(sender,"RES|"..req.id.."|"..(ok and"OK"or"ERR").."|"..Hex(err or""))
        if not ok then Toast(err)end
        Close()
    end)
    if C_Timer and C_Timer.After then
        C_Timer.After(0,function()if root and root:IsShown()then root:Raise();f:Raise()end end)
        C_Timer.After(.05,function()if root and root:IsShown()then root:Raise();f:Raise()end end)
    end
end
function RPAL.HealthOfferTreatment(owner,itemID,action,note,kind,severity,region)
    owner=Trim(owner);if owner==""then Toast("No player is associated with this inspect.");return false end
    local id=RequestID()
    -- Keep each addon packet below WoW's payload ceiling. Text is hex encoded,
    -- so every source byte costs two payload bytes. Severity is sent raw and
    -- body region as a compact index to leave plenty of headroom.
    local safeNote=tostring(note or""):sub(1,56)
    local safeKind=tostring(kind or""):sub(1,30)
    local safeSeverity=InList(SEVERITIES,severity)or"moderate"
    local safeRegion=(tostring(region or""):lower()=="unspecified")and"Unspecified"or(InList(REGIONS,region)or"Unspecified")
    -- Region index 0 is the new backwards-compatible wire representation for
    -- Unspecified. Older builds will simply fall back to their old Auto value.
    local regionIndex=0
    if safeRegion~="Unspecified"then for i,v in ipairs(REGIONS)do if v==safeRegion then regionIndex=i break end end end
    local msg=table.concat({"REQ",id,action or"TREAT",(Trim(itemID)~="" and tostring(itemID) or "-"),Hex(safeNote),Hex(safeKind),safeSeverity,tostring(regionIndex)},"|")
    H2.pending[id]={owner=owner,at=time(),action=action or"TREAT",itemID=itemID};local ok=Send(owner,msg);if ok then
        local sentLabel=({TREAT="Treatment offer",STABILIZE="Stabilize request",NOTE="Medical note request",TEMP="Injury proposal"})[action or"TREAT"]or"Health request"
        Toast(sentLabel.." sent to "..owner..". Waiting for acceptance.")
    else Toast("Could not send Health request.")end;return ok
end
local function PromptRemoteNote(owner,item,action)
    -- Keep the Hotfix 26 Inspect Health layout untouched. Only replace the
    -- old POPOVER text prompt, which could render visibly but lose mouse input
    -- behind the inspect window, with a dedicated modal input layer.
    if H2.actionModal then H2.actionModal:Hide();H2.actionModal:SetParent(nil);H2.actionModal=nil end
    local p=Palette();owner=Trim(owner);action=tostring(action or"TREAT")
    local title,verb,hint,label
    if action=="STABILIZE" then
        title="STABILIZE INJURY";verb="SEND REQUEST";label="NOTE  •  OPTIONAL"
        hint="Marks this injury as stabilized if the owner accepts. It does not skip or change the recovery stage."
    elseif action=="NOTE" then
        title="ADD MEDICAL NOTE";verb="SEND NOTE";label="MEDICAL NOTE  •  REQUIRED"
        hint="Adds a public medical note to this Health record after the owner accepts. It does not change stage or severity."
    else
        title="OFFER TREATMENT";verb="SEND OFFER";label="NOTE  •  OPTIONAL"
        hint="Offers treatment to the owner. If accepted, a Fresh injury advances to Treated; later recovery stages are never moved backwards."
    end
    local root=CreateFrame("Frame",nil,UIParent);H2.actionModal=root;root:SetAllPoints(UIParent)
    if RPAL.ApplyUILayer then RPAL.ApplyUILayer(root,"MODAL",360)else root:SetFrameStrata("FULLSCREEN_DIALOG");root:SetFrameLevel(8600);root:SetToplevel(true)end
    root:EnableMouse(false);root:Show();root:Raise()
    local blocker=CreateFrame("Button",nil,root);blocker:SetAllPoints(root);blocker:SetFrameLevel(root:GetFrameLevel()+1);blocker:EnableMouse(true);blocker:RegisterForClicks("LeftButtonUp","RightButtonUp");blocker:SetScript("OnClick",function()end);blocker:Show()
    local f=CreateFrame("Frame",nil,root,"BackdropTemplate");f:SetSize(520,300);f:SetPoint("CENTER");f:SetFrameLevel(root:GetFrameLevel()+30);f:SetToplevel(true);f:EnableMouse(true);Backdrop(f,p.panel,p.gold);f:Show();f:Raise()
    local function Close()
        if H2.actionModal==root then H2.actionModal=nil end
        if root then root:Hide();root:SetParent(nil);root=nil end
    end
    local t=Font(f,title,16,p.gold);t:SetPoint("TOPLEFT",20,-18)
    local x=Button(f,"X",28,26);x:SetPoint("TOPRIGHT",-14,-12);x:SetFrameLevel(f:GetFrameLevel()+8);x:SetScript("OnClick",Close)
    local target=Font(f,"TARGET  •  "..tostring(owner),7.5,p.muted);target:SetPoint("TOPLEFT",20,-48)
    local record=Font(f,"RECORD  •  "..ItemLabel(item).."  •  "..RPAL.HealthGetRegion(item),8.2,p.pale);record:SetPoint("TOPLEFT",20,-68);record:SetWidth(476);record:SetWordWrap(false)
    local desc=Font(f,hint,8,p.muted);desc:SetPoint("TOPLEFT",20,-94);desc:SetWidth(476);desc:SetWordWrap(true);desc:SetJustifyH("LEFT")
    local noteLabel=Font(f,label,7.2,p.muted);noteLabel:SetPoint("TOPLEFT",20,-142)
    local edit=Edit(f,480,62,true);edit:SetPoint("TOPLEFT",20,-158);edit:SetFrameLevel(f:GetFrameLevel()+8);edit:EnableMouse(true);edit:SetText("")
    local cancel=Button(f,"CANCEL",92,30);cancel:SetPoint("BOTTOMLEFT",20,18);cancel:SetFrameLevel(f:GetFrameLevel()+8);cancel:SetScript("OnClick",Close)
    local send=Button(f,verb,118,30);send:SetPoint("BOTTOMRIGHT",-20,18);send:SetFrameLevel(f:GetFrameLevel()+8)
    send:SetScript("OnClick",function()
        local note=Trim(edit:GetText())
        if action=="NOTE" and note=="" then Toast("Enter a medical note first.");edit:SetFocus();return end
        if RPAL.HealthOfferTreatment(owner,item.id,action,note) then Close() end
    end)
    if C_Timer and C_Timer.After then
        C_Timer.After(0,function()if H2.actionModal==root and root and root:IsShown()then root:Raise();f:Raise()end end)
        C_Timer.After(.05,function()if H2.actionModal==root and root and root:IsShown()then root:Raise();f:Raise()end end)
    end
end
function RPAL.HealthDMInjuryPrompt(target)
    target=Trim(target)
    if target==""then Toast("No player selected for the Health proposal.");return end
    local p=Palette()
    local root=CreateFrame("Frame",nil,UIParent);root:SetAllPoints(UIParent);if RPAL.ApplyUILayer then RPAL.ApplyUILayer(root,"MODAL",250)else root:SetFrameStrata("FULLSCREEN_DIALOG");root:SetFrameLevel(8250);root:SetToplevel(true)end;root:EnableMouse(false);root:Show();root:Raise()
    local blocker=CreateFrame("Button",nil,root);blocker:SetAllPoints(root);blocker:SetFrameLevel(root:GetFrameLevel()+1);blocker:EnableMouse(true);blocker:RegisterForClicks("LeftButtonUp","RightButtonUp");blocker:SetScript("OnClick",function()end);blocker:Show()
    local f=CreateFrame("Frame",nil,root,"BackdropTemplate");f:SetSize(540,430);f:SetPoint("CENTER");f:SetFrameLevel(root:GetFrameLevel()+30);f:SetToplevel(true);f:EnableMouse(true);Backdrop(f,p.panel,p.gold);f:Show();f:Raise()
    local function Close()if root then root:Hide();root:SetParent(nil);root=nil end end
    local title=Font(f,"PROPOSE INJURY",16,p.gold);title:SetPoint("TOPLEFT",18,-18)
    local sub=Font(f,"The player must accept this Health record. Accepted injuries persist until the player changes or removes them.",8.5,p.muted);sub:SetPoint("TOPLEFT",18,-46);sub:SetWidth(500);sub:SetWordWrap(true)

    local y=-92
    local function Label(text)local l=Font(f,text,8,p.muted);l:SetPoint("TOPLEFT",18,y);y=y-18;return l end
    local injuryOptions={}
    local foundBroken=false
    for _,name in ipairs(RPAL.INJURY_TYPES or{})do injuryOptions[#injuryOptions+1]={value=name,label=name};if name=="Broken"then foundBroken=true end end
    injuryOptions[#injuryOptions+1]={value="__CUSTOM__",label="Custom..."}
    local selectedKind=foundBroken and"Broken"or((injuryOptions[1]and injuryOptions[1].value)or"Injury")
    Label("INJURY TYPE")
    local typeDrop=Dropdown(f,selectedKind,382,injuryOptions,function(v,label)
        selectedKind=v
        if f.customEdit then f.customEdit:SetShown(v=="__CUSTOM__") end
        return label
    end);typeDrop:SetPoint("TOPLEFT",18,y);y=y-38
    local custom=Edit(f,382,30,false);f.customEdit=custom;custom:SetPoint("TOPLEFT",18,y);custom:SetText("");custom:Hide();y=y-38

    local selectedSeverity="moderate"
    Label("SEVERITY")
    local sevOptions={};for _,v in ipairs(SEVERITIES)do sevOptions[#sevOptions+1]={value=v,label=string.upper(v)}end
    local sevDrop=Dropdown(f,"MODERATE",220,sevOptions,function(v,label)selectedSeverity=v;return label end);sevDrop:SetPoint("TOPLEFT",18,y);y=y-38

    local selectedRegion="Unspecified"
    Label("BODY REGION")
    local regionOptions={{value="Unspecified",label="Unspecified"}}
    for _,v in ipairs(REGIONS)do if v~="Auto"then regionOptions[#regionOptions+1]={value=v,label=v}end end
    local regionDrop=Dropdown(f,"Unspecified",300,regionOptions,function(v,label)selectedRegion=v;return label end);regionDrop:SetPoint("TOPLEFT",18,y);y=y-38

    Label("DM NOTE  •  OPTIONAL")
    local note=Edit(f,500,46,true);note:SetPoint("TOPLEFT",18,y);y=y-58

    local cancel=Button(f,"CANCEL",90,28);cancel:SetPoint("BOTTOMLEFT",18,18);cancel:SetFrameLevel(f:GetFrameLevel()+5);cancel:SetScript("OnClick",Close)
    local save=Button(f,"SEND PROPOSAL",126,28);save:SetPoint("BOTTOMRIGHT",-18,18);save:SetFrameLevel(f:GetFrameLevel()+5)
    save:SetScript("OnClick",function()
        local kind=selectedKind
        if kind=="__CUSTOM__"then kind=Trim(custom:GetText());if kind==""then Toast("Enter a custom injury name.");custom:Show();custom:SetFocus();return end end
        local ok=RPAL.HealthOfferTreatment(target,"","TEMP",Trim(note:GetText()),kind,selectedSeverity,selectedRegion)
        -- Do not leave a dead modal sitting over the DM screen.  Failed sends
        -- already produce a toast and can simply be retried by reopening it.
        Close()
    end)
    if C_Timer and C_Timer.After then C_Timer.After(0,function()if root and root:IsShown()then root:Raise();f:Raise()end end)end
end
if C_ChatInfo and C_ChatInfo.RegisterAddonMessagePrefix then pcall(C_ChatInfo.RegisterAddonMessagePrefix,PREFIX) end
local net=CreateFrame("Frame");net:RegisterEvent("CHAT_MSG_ADDON");net:SetScript("OnEvent",function(_,_,prefix,msg,channel,sender)
    if prefix~=PREFIX or channel~="WHISPER" or not sender then return end;local f={};for x in tostring(msg):gmatch("[^|]+")do f[#f+1]=x end
    if f[1]=="REQ"then local regionIndex=tonumber(f[8]);local wireRegion=(regionIndex==0)and"Unspecified"or(REGIONS[regionIndex or 1]or"Unspecified");local req={id=f[2],action=f[3],itemID=f[4],note=Unhex(f[5]),kind=Unhex(f[6]),severity=InList(SEVERITIES,f[7])or"moderate",region=wireRegion};IncomingPrompt(req,sender)
    elseif f[1]=="RES"then local pending=H2.pending[f[2]];if pending then H2.pending[f[2]]=nil;local label=({TREAT="Treatment offer",STABILIZE="Stabilize request",NOTE="Medical note",TEMP="Injury proposal"})[pending.action or""]or"Health request";Toast(f[3]=="OK"and(label.." accepted by "..sender..".")or(f[3]=="NO"and(label.." declined by "..sender..".")or(label.." failed: "..Unhex(f[4]))))end
    elseif f[1]=="SUM"and RPAL.DMKit and RPAL.DMKit.state and RPAL.DMKit.state.role=="HOST"then local state=RPAL.DMKit.state;state.healthWarnings=state.healthWarnings or{};state.healthWarnings[sender]=Unhex(f[2])
    end
end)

-- ---------- read-only inspect renderer with active/history tabs ----------
local BODY_TEX="Interface\\AddOns\\"..tostring(ADDON_NAME or"ODYN").."\\Textures\\humanbody.png"
local function ReadOnlyMarkerColor(item,p,accent)
    if item.category=="alteration" then return p.alteration or accent end
    if item.category=="healing" then return p.heal or accent end
    local red=p.red or accent;local heal=p.heal or accent;local gold=p.gold or accent;local muted=p.muted or accent
    local stage=tostring(item.stage or "fresh"):lower()
    if stage=="treated" then return {(red[1]*.55)+(gold[1]*.45),(red[2]*.55)+(gold[2]*.45),(red[3]*.55)+(gold[3]*.45),1} end
    if stage=="recovering" then return {(red[1]*.28)+(heal[1]*.72),(red[2]*.28)+(heal[2]*.72),(red[3]*.28)+(heal[3]*.72),1} end
    if stage=="healed" then return {(muted[1]*.72)+(heal[1]*.28),(muted[2]*.72)+(heal[2]*.28),(muted[3]*.72)+(heal[3]*.28),.82} end
    return red
end
local function RORotateXY(x,y,radians)local c,s=math.cos(radians),math.sin(radians);return x*c-y*s,x*s+y*c end
local function ROSetPoint(region,host,x,y,radians)
    local rx,ry=RORotateXY(x,y,radians);region:ClearAllPoints();region:SetPoint("CENTER",host,"CENTER",rx,ry);if region.SetRotation then region:SetRotation(radians)end
end
local function BuildReadOnlyMarker(host,w,h,color,rotation,label,p)
    local rad=math.rad(tonumber(rotation)or 0)
    local bg=host:CreateTexture(nil,"BACKGROUND",nil,-2);bg:SetTexture("Interface\\Buttons\\WHITE8X8");bg:SetSize(w,h);bg:SetPoint("CENTER");bg:SetVertexColor(.06,.045,.04,.94);if bg.SetRotation then bg:SetRotation(rad)end
    local tint=host:CreateTexture(nil,"BACKGROUND",nil,-1);tint:SetTexture("Interface\\Buttons\\WHITE8X8");tint:SetSize(math.max(1,w-8),math.max(1,h-8));tint:SetPoint("CENTER");tint:SetBlendMode("ADD");tint:SetVertexColor(color[1],color[2],color[3],.16);if tint.SetRotation then tint:SetRotation(rad)end
    local edges={}
    for n=1,4 do edges[n]=host:CreateTexture(nil,"ARTWORK");edges[n]:SetTexture("Interface\\Buttons\\WHITE8X8");edges[n]:SetVertexColor(color[1],color[2],color[3],1)end
    edges[1]:SetSize(w,1);edges[2]:SetSize(w,1);edges[3]:SetSize(1,h);edges[4]:SetSize(1,h)
    ROSetPoint(edges[1],host,0,h*.5,rad);ROSetPoint(edges[2],host,0,-h*.5,rad);ROSetPoint(edges[3],host,-w*.5,0,rad);ROSetPoint(edges[4],host,w*.5,0,rad)
    local tx=Font(host,label,7.2,p.pale);tx:SetWidth(math.max(30,w-8));tx:SetHeight(14);tx:SetJustifyH("CENTER");tx:SetWordWrap(false);ROSetPoint(tx,host,0,0,rad)
end

local function ApplyReadOnlyAnatomy(body,image,a,side,accent)
    local st=PRESET_STYLE[(a and a.preset)or"Humanoid"]or PRESET_STYLE.Humanoid;image:ClearAllPoints();image:SetPoint("CENTER");image:SetSize(body:GetWidth()*st.w,body:GetHeight()*st.h);image:SetTexCoord(side=="back"and 1 or 0,side=="back"and 0 or 1,0,1);local traits=Copy(PRESET_TRAITS[(a and a.preset)or"Humanoid"]or{});for k,v in pairs(a and a.traits or{})do traits[k]=v end;DrawTraits(body,traits,side,accent)
end
function RPAL.RenderInjuryReadOnly(parent,data,accent,owner)
    if not parent then return end;local p=Palette();accent=accent or p.gold;data=data or{items={},groups={},history={}};local width=parent:GetWidth()or 760;local host=CreateFrame("Frame",nil,parent);host:SetSize(width,540);host:SetPoint("TOPLEFT");local title=Font(host,"HEALTH",15,accent);title:SetPoint("TOPLEFT",18,-14);local an=Font(host,string.upper((data.anatomy and data.anatomy.preset)or"Humanoid").." BODY MAP",7.5,p.muted);an:SetPoint("TOPLEFT",18,-35)
    local active=Button(host,"ACTIVE",72,22);active:SetPoint("TOPRIGHT",-90,-12);local hist=Button(host,"HISTORY",72,22);hist:SetPoint("TOPRIGHT",-12,-12)
    local activePane=CreateFrame("Frame",nil,host);activePane:SetPoint("TOPLEFT",0,-46);activePane:SetPoint("BOTTOMRIGHT",0,0);local historyPane=CreateFrame("Frame",nil,host);historyPane:SetAllPoints(activePane);historyPane:Hide()
    active:SetScript("OnClick",function()activePane:Show();historyPane:Hide();active:SetAlpha(1);hist:SetAlpha(.55)end);hist:SetScript("OnClick",function()activePane:Hide();historyPane:Show();active:SetAlpha(.55);hist:SetAlpha(1)end);hist:SetAlpha(.55)
    -- The editable Health screen stores every node against a fixed 220x396
    -- coordinate canvas.  Inspect used to rebuild that canvas at an arbitrary
    -- aspect/size, so the same saved x/y landed on the wrong anatomical spot.
    -- Keep the source canvas *identical* and only scale the whole canvas
    -- uniformly for presentation.  This also makes /odyn mirror health a true
    -- preview of what another player sees.
    local sourceBodyW,sourceBodyH=220,396
    local bodyScale=1.16
    if width<1040 then bodyScale=1.08 end
    if width<900 then bodyScale=.96 end
    if width<790 then bodyScale=.86 end
    local displayBodyW=math.floor(sourceBodyW*bodyScale+.5)
    local displayBodyH=math.floor(sourceBodyH*bodyScale+.5)
    local wrapW=displayBodyW+24
    local wrapH=displayBodyH+44
    local bodyPanelW=(wrapW*2)+34
    local bodyPanelH=wrapH+12
    local listLeft=bodyPanelW+24
    local listWidth=width-listLeft-12
    if listWidth<285 then
        bodyScale=math.max(.78,bodyScale-((285-listWidth)/420))
        displayBodyW=math.floor(sourceBodyW*bodyScale+.5)
        displayBodyH=math.floor(sourceBodyH*bodyScale+.5)
        wrapW=displayBodyW+24;wrapH=displayBodyH+44
        bodyPanelW=(wrapW*2)+34;bodyPanelH=wrapH+12
        listLeft=bodyPanelW+24;listWidth=math.max(265,width-listLeft-12)
    end
    local bp=CreateFrame("Frame",nil,activePane,"BackdropTemplate");bp:SetSize(bodyPanelW,bodyPanelH);bp:SetPoint("TOPLEFT",8,-4);Backdrop(bp,{.035,.03,.025,.98},accent)
    local function Body(side,x)
        local wrap=CreateFrame("Frame",nil,bp,"BackdropTemplate");wrap:SetSize(wrapW,wrapH);wrap:SetPoint("LEFT",x,0);Backdrop(wrap,{.025,.022,.019,.98},accent)
        local label=Font(wrap,string.upper(side),8,accent);label:SetPoint("TOP",0,-7)
        local b=CreateFrame("Frame",nil,wrap);b:SetSize(sourceBodyW,sourceBodyH);b:SetScale(bodyScale);b:SetPoint("CENTER",0,-10)
        local im=b:CreateTexture(nil,"BACKGROUND");im:SetTexture(BODY_TEX);ApplyReadOnlyAnatomy(b,im,data.anatomy,side,accent)
        return b
    end
    local fb=Body("front",9);local bb=Body("back",wrapW+25)
    for _,i in pairs(data.items or{})do
        local b=i.side=="back"and bb or fb;local c=ReadOnlyMarkerColor(i,p,accent)
        -- Match the editor marker footprint (104x40 artwork) and place its
        -- centre against the exact same 220x396 source canvas. Parent scaling
        -- enlarges body + marker together without changing saved coordinates.
        -- IMPORTANT: the saved x/y belongs to an unscaled anchor frame.  The
        -- editable Health UI keeps its 112x48 interaction frame at scale 1 and
        -- scales only the 104x40 visual child. Scaling the anchored frame itself
        -- makes WoW rescale its anchor coordinate too, which is why 65% markers
        -- appeared to slide away from the hands in remote/mirror inspect.
        local m=CreateFrame("Frame",nil,b);m:SetSize(112,48);m:SetScale(1);m:SetPoint("CENTER",b,"TOPLEFT",Clamp(i.x,.02,.98)*sourceBodyW,-Clamp(i.y,.02,.98)*sourceBodyH)
        local visual=CreateFrame("Frame",nil,m);visual:SetSize(104,40);visual:SetPoint("CENTER");visual:SetScale(Clamp(i.scale or 1,.65,1.55))
        BuildReadOnlyMarker(visual,104,40,c,i.rotation or 0,ItemLabel(i),p)
    end
    local list=CreateFrame("ScrollFrame",nil,activePane,"UIPanelScrollFrameTemplate");list:SetPoint("TOPLEFT",listLeft,-4);list:SetPoint("TOPRIGHT",-10,-4);list:SetHeight(bodyPanelH);local c=CreateFrame("Frame",nil,list);c:SetWidth(math.max(170,listWidth-22));c:SetHeight(1);list:SetScrollChild(c);local y=-2;local arr={};for _,i in pairs(data.items or{})do arr[#arr+1]=i end;table.sort(arr,function(a,b)return ItemLabel(a)<ItemLabel(b)end)
    if RPAL.SkinScrollFrame then RPAL.SkinScrollFrame(list) end
    local function DetailText(parent,text,size,color,x,y,w,maxH)
        local fs=Font(parent,tostring(text or ""),size,color)
        fs:SetPoint("TOPLEFT",x,y);fs:SetWidth(w);fs:SetJustifyH("LEFT");fs:SetJustifyV("TOP");fs:SetWordWrap(true)
        local measured=(fs.GetStringHeight and fs:GetStringHeight()) or 12
        local h=math.max(12,math.min(maxH or 36,math.ceil(measured+2)))
        fs:SetHeight(h)
        return fs,h
    end
    local function ROFormatRemaining(seconds)
        local s=math.max(0,tonumber(seconds)or 0)
        if s>=86400 then return math.floor(s/86400).."d "..math.floor((s%86400)/3600).."h" end
        if s>=3600 then return math.floor(s/3600).."h "..math.floor((s%3600)/60).."m" end
        return math.max(1,math.floor(s/60)).."m"
    end
    local function CategoryLabel(item)
        if item.category=="alteration" then return "ALTERATION" end
        if item.category=="healing" then return "HEALING STATUS" end
        return "INJURY"
    end
    for _,i in ipairs(arr)do
        local cardColor=ReadOnlyMarkerColor(i,p,accent)
        local note=(i.note and Trim(i.note)~="")and Trim(i.note)or""
        local effects=SymptomsText(i)
        local meds=i.medicalNotes or{}
        local r=CreateFrame("Frame",nil,c,"BackdropTemplate");r:SetWidth(c:GetWidth()-4);r:SetPoint("TOPLEFT",2,y);Backdrop(r,p.panel2,p.border)
        local stripe=r:CreateTexture(nil,"ARTWORK");stripe:SetTexture("Interface\\Buttons\\WHITE8X8");stripe:SetPoint("TOPLEFT",1,-1);stripe:SetPoint("BOTTOMLEFT",1,1);stripe:SetWidth(3);stripe:SetVertexColor(cardColor[1],cardColor[2],cardColor[3],.95)
        local titleText=ItemLabel(i);if Trim(titleText)=="" then titleText=CategoryLabel(i) end
        local nm=Font(r,titleText,11,p.pale);nm:SetPoint("TOPLEFT",13,-10);nm:SetWidth(math.max(120,c:GetWidth()-128));nm:SetJustifyH("LEFT");nm:SetWordWrap(false)
        local cat=Font(r,CategoryLabel(i),7.2,cardColor);cat:SetPoint("TOPRIGHT",-10,-12);cat:SetJustifyH("RIGHT")
        local cursor=-31
        if i.category=="injury" then
            local stage=string.upper(Trim(i.stage)~="" and i.stage or "fresh")
            local sev=string.upper(Trim(i.severity)~="" and i.severity or "moderate")
            local status=Font(r,stage.."  •  "..sev..(i.stabilized and "  •  STABILIZED" or ""),7.4,accent);status:SetPoint("TOPLEFT",13,cursor);status:SetWidth(c:GetWidth()-24);status:SetJustifyH("LEFT");cursor=cursor-18
        elseif i.category=="alteration" then
            local status=Font(r,"PERMANENT CHANGE",7.2,accent);status:SetPoint("TOPLEFT",13,cursor);cursor=cursor-18
        elseif i.category=="healing" and i.remaining then
            local status=Font(r,"ACTIVE  •  "..ROFormatRemaining(i.remaining).." REMAINING",7.2,accent);status:SetPoint("TOPLEFT",13,cursor);cursor=cursor-18
        end
        local region=Trim(i.region);if region=="" then region=RPAL.HealthGetRegion(i) end
        local regionLabel=Font(r,"LOCATION",6.5,p.muted);regionLabel:SetPoint("TOPLEFT",13,cursor);local regionValue=Font(r,region,7.6,p.pale);regionValue:SetPoint("TOPLEFT",72,cursor+1);regionValue:SetWidth(c:GetWidth()-86);regionValue:SetJustifyH("LEFT");cursor=cursor-19
        if effects~="" then
            local lab=Font(r,"EFFECTS",6.5,cardColor);lab:SetPoint("TOPLEFT",13,cursor);cursor=cursor-13
            local _,hh=DetailText(r,effects,7.1,p.pale,13,cursor,c:GetWidth()-26,34);cursor=cursor-hh-8
        end
        if note~="" then
            local lab=Font(r,"NOTE",6.5,cardColor);lab:SetPoint("TOPLEFT",13,cursor);cursor=cursor-13
            local _,hh=DetailText(r,note,7.1,p.pale,13,cursor,c:GetWidth()-26,44);cursor=cursor-hh-8
        end
        local shown=math.min(2,#meds)
        if shown>0 then
            local lab=Font(r,"MEDICAL NOTES",6.5,cardColor);lab:SetPoint("TOPLEFT",13,cursor);cursor=cursor-14
            for n=1,shown do
                local mn=meds[n]
                local author=Trim(mn.author);if author=="" then author="Unknown" end
                local text=author..": "..Trim(mn.text)
                local _,hh=DetailText(r,text,6.8,p.muted,13,cursor,c:GetWidth()-26,27);cursor=cursor-hh-6
            end
        end
        if i.originId or Trim(i.originLabel)~="" then
            local source=(i.originId and data.items and data.items[tostring(i.originId)]) or nil
            local linked=Trim(i.originLabel)~="" and Trim(i.originLabel) or (source and ItemLabel(source)) or "Linked health event"
            local lab=Font(r,"RESULT OF",6.2,p.muted);lab:SetPoint("TOPLEFT",13,cursor);local val=Font(r,linked,6.8,p.muted);val:SetPoint("TOPLEFT",72,cursor);val:SetWidth(c:GetWidth()-86);val:SetJustifyH("LEFT");cursor=cursor-18
        end
        local buttonSpace=(owner and i.category=="injury")and 34 or 8
        local cardH=math.max(82,-cursor+buttonSpace+4);r:SetHeight(cardH)
        if owner and i.category=="injury"then
            local rule=r:CreateTexture(nil,"ARTWORK");rule:SetTexture("Interface\\Buttons\\WHITE8X8");rule:SetPoint("BOTTOMLEFT",10,34);rule:SetPoint("BOTTOMRIGHT",-10,34);rule:SetHeight(1);rule:SetVertexColor(p.border[1],p.border[2],p.border[3],.55)
            local treat=Button(r,"TREAT",62,22);treat:SetPoint("BOTTOMLEFT",10,7);treat:SetScript("OnClick",function()PromptRemoteNote(owner,i,"TREAT")end)
            local stab=Button(r,"STABILIZE",82,22);stab:SetPoint("LEFT",treat,"RIGHT",6,0);stab:SetScript("OnClick",function()PromptRemoteNote(owner,i,"STABILIZE")end)
            local noteB=Button(r,"ADD NOTE",74,22);noteB:SetPoint("LEFT",stab,"RIGHT",6,0);noteB:SetScript("OnClick",function()PromptRemoteNote(owner,i,"NOTE")end)
        end
        y=y-cardH-8
    end
    if #arr==0 then
        local empty=Font(c,"No active Health records shared.",9,p.muted);empty:SetPoint("TOPLEFT",12,-18);empty:SetWidth(c:GetWidth()-24);empty:SetJustifyH("LEFT");y=-62
    end
    c:SetHeight(math.max(1,-y+12))
    local ihTitle=Font(historyPane,"PUBLIC MEDICAL HISTORY",10,accent);ihTitle:SetPoint("TOPLEFT",12,-7)
    local publicCount=#(data.history or{});local ihSub=Font(historyPane,string.format("%d shared event%s  •  newest first  •  click a day to collapse",publicCount,publicCount==1 and""or"s"),7,p.muted);ihSub:SetPoint("TOPLEFT",12,-25)
    local hs=CreateFrame("ScrollFrame",nil,historyPane,"UIPanelScrollFrameTemplate");hs:SetPoint("TOPLEFT",8,-46);hs:SetPoint("BOTTOMRIGHT",-24,4)
    if RPAL.SkinScrollFrame then RPAL.SkinScrollFrame(hs) end
    historyPane._collapsedDays=historyPane._collapsedDays or{}
    local function RenderSharedHistory()
        if historyPane._historyContent then historyPane._historyContent:Hide();historyPane._historyContent:SetParent(nil)end
        local hc=CreateFrame("Frame",nil,hs);historyPane._historyContent=hc;hc:SetWidth(math.max(240,width-46));hc:SetHeight(1);hs:SetScrollChild(hc)
        local hy=-6
        if publicCount>0 then
            local line=hc:CreateTexture(nil,"BACKGROUND");line:SetColorTexture(p.border[1],p.border[2],p.border[3],.8);line:SetWidth(1);line:SetPoint("TOPLEFT",23,-32);line:SetPoint("BOTTOMLEFT",23,12)
            local groups=GroupHistory(data.history or{},"all",p,60)
            for gi,group in ipairs(groups)do
                if gi>1 then hy=hy-6 end
                local collapsed=historyPane._collapsedDays[group.key] and true or false
                BuildHistoryDateHeader(hc,group,hy,p,accent,collapsed,function(key)historyPane._collapsedDays[key]=not historyPane._collapsedDays[key];RenderSharedHistory()end)
                hy=hy-26
                if not collapsed then
                    for _,e in ipairs(group.events)do local r,dot,arm,h=BuildHistoryEventCard(hc,e,math.max(250,width-108),hy,p,accent);hy=hy-h-9 end
                end
            end
        else
            local e=Font(hc,"No public medical history shared.",9,p.muted);e:SetPoint("TOPLEFT",12,-18);e:SetWidth(width-80);e:SetJustifyH("LEFT");hy=-70
        end
        hc:SetHeight(math.max(1,-hy+12))
    end
    RenderSharedHistory();host:SetHeight(math.max(520,bodyPanelH+96));return host,bp,list
end

-- ---------- session snapshots ----------
local function StateMap()
    local d=DB();local m={}
    for id,i in pairs(d.items or{})do
        if i.category~="healing"then m[id]={label=ItemLabel(i),category=i.category,stage=i.stage,severity=i.severity,region=RPAL.HealthGetRegion(i),symptoms=SymptomsText(i),privacy=i.privacy}end
    end
    return m
end
local function DiffState(before,after)
    local out={}
    for id,a in pairs(after)do
        local b=before[id]
        if not b then out[#out+1]="+ "..a.label
        else
            if b.stage~=a.stage then out[#out+1]=a.label.." "..tostring(b.stage or"").." → "..tostring(a.stage or"") end
            if b.severity~=a.severity then out[#out+1]=a.label.." severity "..tostring(b.severity or"").." → "..tostring(a.severity or"") end
            if b.region~=a.region then out[#out+1]=a.label.." region "..tostring(b.region or"").." → "..tostring(a.region or"") end
            if b.symptoms~=a.symptoms then out[#out+1]=a.label.." effects updated" end
        end
    end
    for id,b in pairs(before)do if not after[id]then out[#out+1]="− "..b.label end end
    return out
end
local oldStarted=RPAL.OnSceneStarted;RPAL.OnSceneStarted=function(scene)if oldStarted then oldStarted(scene)end;if scene then scene._healthStartSnapshot=StateMap()end end
local oldPaused=RPAL.OnScenePaused;RPAL.OnScenePaused=function(scene)if scene and not scene._healthStartSnapshot then scene._healthStartSnapshot=StateMap()end;if oldPaused then oldPaused(scene)end end
local oldEnded=RPAL.OnSceneEnded;RPAL.OnSceneEnded=function(scene)
    if scene then local before=scene._healthStartSnapshot or{};local changes=DiffState(before,StateMap());scene.healthChanges=changes;scene._healthStartSnapshot=nil;if #changes>0 then local d=DB();d.history=d.history or{};do local now=time();table.insert(d.history,1,{id=NewHistoryID(now),at=now,kind="session-snapshot",label=scene.title or"Session",note=table.concat(changes," • "),privacy="odyn",sceneID=scene.id,sceneTitle=scene.title}) end end end;if oldEnded then oldEnded(scene)end
end

-- ---------- DM awareness / roll warnings / persistent proposed injuries ----------
local function SevereSummary()
    local d=DB();if not d then return "" end;local out={};for _,i in pairs(d.items or{})do if i.category=="injury"and(i.severity=="severe"or i.severity=="critical")and i.stage~="healed"then out[#out+1]=string.upper(i.severity).." "..ItemLabel(i).." ("..RPAL.HealthGetRegion(i)..")"end end;table.sort(out);return table.concat(out,"; ")
end
local DM=RPAL.DMKit
if DM and DM.StartRollCheck then
    local old=DM.StartRollCheck;DM.StartRollCheck=function(title,info,sides,minimum,targets,options)
        local state=DM.state;local warnings={};if state and state.role=="HOST"and state.healthWarnings then for _,name in ipairs(targets or{})do local hit=state.healthWarnings[name];if not hit then for key,v in pairs(state.healthWarnings)do if tostring(key):match("^[^-]+") == tostring(name):match("^[^-]+")then hit=v break end end end;if hit and hit~=""then warnings[#warnings+1]=tostring(name)..": "..hit end end end
        if #warnings>0 then info=tostring(info or"").."\n\nHEALTH ADVISORY (no automatic penalty):\n"..table.concat(warnings,"\n")end;return old(title,info,sides,minimum,targets,options)
    end
end
-- DM-proposed injuries are normal persistent Health records after consent.
-- No encounter/session-end cleanup is performed.

-- ---------- HUD + heartbeat ----------
local function EnsureHUD()
    if H2.hud or not RPAL.UI or not RPAL.UI.main then return end;local main=RPAL.UI.main;local p=Palette();local b=CreateFrame("Button",nil,main,"BackdropTemplate");b:SetSize(28,28);b:SetPoint("TOPRIGHT",-56,-18);Backdrop(b,p.panel2,p.border);local icon=b:CreateTexture(nil,"ARTWORK");icon:SetTexture("Interface\\AddOns\\"..tostring(ADDON_NAME).."\\Textures\\IconPack\\warning.tga");icon:SetSize(18,18);icon:SetPoint("CENTER");icon:SetVertexColor(unpack(p.gold));b:SetScript("OnClick",function()if RPAL.OpenInjurySystem then RPAL.OpenInjurySystem()end end);b:SetScript("OnEnter",function(self)GameTooltip:SetOwner(self,"ANCHOR_BOTTOM");GameTooltip:SetText("ODYN Health");local s,c=RPAL.GetHealthHUDSummary();GameTooltip:AddLine(s,1,1,1);if c.critical>0 then GameTooltip:AddLine(tostring(c.critical).." critical",1,.25,.2)end;GameTooltip:Show()end);b:SetScript("OnLeave",function()GameTooltip:Hide()end);H2.hud=b
end
function RPAL.RefreshHealthHUD()EnsureHUD();if not H2.hud then return end;local _,c=RPAL.GetHealthHUDSummary();H2.hud:SetShown((c.injuries+c.permanent+c.healed)>0)end
local function HealthHeartbeat()
    if not RPAL.char then return end
    EnsureHealthChrome();RPAL.RefreshHealthHUD()
    local state=RPAL.DMKit and RPAL.DMKit.state
    if state and state.role=="PARTICIPANT"and state.host then
        local sum=SevereSummary();local fingerprint=tostring(state.host).."\031"..sum;local now=time()
        -- Health advisories are state, not telemetry. Send immediately when
        -- they change and only a small lease refresh while unchanged instead
        -- of broadcasting the same payload every few seconds forever.
        if H2.lastDMSummary~=fingerprint or now-(tonumber(H2.lastDMSummaryAt)or 0)>=30 then
            Send(state.host,"SUM|"..Hex(sum:sub(1,108)));H2.lastDMSummary=fingerprint;H2.lastDMSummaryAt=now
        end
    else
        H2.lastDMSummary=nil;H2.lastDMSummaryAt=nil
    end
    H2.lastDMState=state and true or false
    for id,pending in pairs(H2.pending)do if time()-(pending.at or 0)>30 then Toast("Health request to "..tostring(pending.owner or "player").." timed out.");H2.pending[id]=nil end end
end

local healthHeartbeatEvent=CreateFrame("Frame")
healthHeartbeatEvent:RegisterEvent("PLAYER_LOGIN")
healthHeartbeatEvent:SetScript("OnEvent",function()
    HealthHeartbeat()
    if C_Timer and C_Timer.NewTicker and not H2.heartbeatTicker then H2.heartbeatTicker=C_Timer.NewTicker(10,HealthHeartbeat) end
end)
if not C_Timer or not C_Timer.NewTicker then
    local acc=0;healthHeartbeatEvent:SetScript("OnUpdate",function(_,elapsed)acc=acc+(elapsed or 0);if acc>=10 then acc=0;HealthHeartbeat()end end)
end
