local ADDON_NAME, RPAL = ...

-- ============================================================
-- Story Arc board + Watch presentation
-- ============================================================

local FONT=STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF"
local DISPLAY=FONT
local WHITE="Interface\\Buttons\\WHITE8X8"
local SPARK="Interface\\AddOns\\ODYN\\Textures\\Talent\\sparkle.tga"
local BOARD_W=RPAL.STORY_ARC_BOARD_WIDTH or 1800
local BOARD_H=RPAL.STORY_ARC_BOARD_HEIGHT or 1100
local NODE_W,NODE_H=190,122

local activeBoardShade
local activeWatchShade
local storyModalSerial=0

local function Palette()
    local p=RPAL.GetCurrentThemePalette and RPAL.GetCurrentThemePalette() or {}
    return {
        bg=p.main or {0.022,0.016,0.034,0.99},
        panel=p.panel or {0.050,0.034,0.064,0.98},
        panel2=p.panel2 or {0.080,0.052,0.075,0.98},
        border=p.border or {0.30,0.22,0.32,1},
        button=p.button or {0.080,0.052,0.075,1},
        hover=p.buttonHover or {0.14,0.09,0.15,1},
        accent=p.accent or {0.20,0.12,0.04,1},
        gold=p.gold or {0.93,0.72,0.27,1},
        pale=p.pale or {0.94,0.90,0.82,1},
        muted=p.muted or {0.62,0.59,0.63,1},
        green={0.40,0.88,0.58,1},
        red={0.92,0.38,0.38,1},
        blue={0.35,0.68,1.00,1},
    }
end

local function Scaled(value)
    return RPAL.GetScaledFontSize and RPAL.GetScaledFontSize(value) or value
end

local function Text(parent,value,size,color,font)
    local f=parent:CreateFontString(nil,"OVERLAY")
    f:SetFont(font or FONT,Scaled(size or 11),"")
    f:SetText(tostring(value or ""))
    color=color or Palette().pale
    f:SetTextColor(color[1],color[2],color[3],color[4] or 1)
    f:SetShadowColor(0,0,0,0.95);f:SetShadowOffset(1,-1)
    return f
end

local function Backdrop(frame,bg,border,edge)
    frame:SetBackdrop({
        bgFile="Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",
        tile=false,tileSize=16,edgeSize=edge or 14,
        insets={left=3,right=3,top=3,bottom=3},
    })
    bg=bg or Palette().panel;border=border or Palette().border
    frame:SetBackdropColor(bg[1],bg[2],bg[3],bg[4] or 1)
    frame:SetBackdropBorderColor(border[1],border[2],border[3],border[4] or 1)
end

local function Button(parent,label,w,h,accent)
    local C=Palette()
    local b=CreateFrame("Button",nil,parent,"BackdropTemplate")
    b:SetSize(w or 96,h or 30)
    Backdrop(b,accent and C.accent or C.button,accent and C.gold or C.border)
    b.label=Text(b,label,9.5,accent and C.gold or C.pale)
    b.label:SetPoint("CENTER")
    b:SetScript("OnEnter",function(self)
        local color=accent and C.accent or C.hover
        self:SetBackdropColor(color[1],color[2],color[3],1)
        self:SetBackdropBorderColor(C.gold[1],C.gold[2],C.gold[3],0.95)
    end)
    b:SetScript("OnLeave",function(self)
        Backdrop(self,accent and C.accent or C.button,accent and C.gold or C.border)
    end)
    if RPAL.AttachButtonMotion then RPAL.AttachButtonMotion(b,0.018) end
    if RPAL.AttachFocusMark then RPAL.AttachFocusMark(b,C.gold) end
    return b
end

local function EditBox(parent,w,h,multiline)
    local C=Palette()
    local e=CreateFrame("EditBox",nil,parent,"BackdropTemplate")
    e:SetSize(w,h);Backdrop(e,C.panel2,C.border)
    e:SetFont(FONT,Scaled(10.5),"");e:SetTextColor(C.pale[1],C.pale[2],C.pale[3])
    e:SetTextInsets(9,9,multiline and 8 or 0,multiline and 8 or 0)
    e:SetAutoFocus(false);e:SetMultiLine(multiline and true or false)
    return e
end

local function Modal(title,w,h)
    local C=Palette()
    if RPAL.HideCursorTooltip then RPAL.HideCursorTooltip() end
    if GameTooltip and GameTooltip.Hide then GameTooltip:Hide() end
    if RPAL.CreateModal then
        local shade,f=RPAL.CreateModal(title,w or 620,h or 480)
        shade._rpalCloseButton=f and f._rpalModalClose
        shade._rpalStoryModalLevel=shade.GetFrameLevel and shade:GetFrameLevel() or 0
        shade._rpalEscapeFailsafe=true
        return shade,f
    end
    storyModalSerial=storyModalSerial+1
    local level=200+(storyModalSerial*8)
    local shade=CreateFrame("Frame","RPAL_StoryModal_"..tostring(storyModalSerial),UIParent,"BackdropTemplate")
    shade:SetAllPoints();shade:SetFrameStrata("TOOLTIP");shade:SetFrameLevel(level)
    if shade.SetToplevel then shade:SetToplevel(true) end
    shade:EnableMouse(true);shade:SetBackdrop({bgFile=WHITE});shade:SetBackdropColor(0,0,0,0.76)
    local f=CreateFrame("Frame",nil,shade,"BackdropTemplate")
    f:SetSize(w or 620,h or 480);f:SetPoint("CENTER");f:SetFrameStrata("TOOLTIP");f:SetFrameLevel(level+4)
    if f.SetToplevel then f:SetToplevel(true) end
    Backdrop(f,C.bg,C.gold,18)
    local glow=f:CreateTexture(nil,"BACKGROUND",nil,-1)
    glow:SetTexture(SPARK);glow:SetBlendMode("ADD");glow:SetSize((w or 620)*0.72,180)
    glow:SetPoint("TOP",0,40);glow:SetVertexColor(C.gold[1],C.gold[2],C.gold[3],0.18)
    local heading=Text(f,title,18,C.gold,DISPLAY);heading:SetPoint("TOPLEFT",20,-18)
    local close=Button(f,"×",34,28,false);close:SetPoint("TOPRIGHT",-12,-12)
    close.label:SetFont(DISPLAY,Scaled(18),"");close:SetScript("OnClick",function()shade:Hide()end)
    shade._rpalCloseButton=close
    shade._rpalStoryModalLevel=level
    if RPAL.RegisterEscapeFrame then RPAL.RegisterEscapeFrame(shade) end
    shade._rpalEscapeFailsafe=true
    if shade.Raise then shade:Raise() end
    return shade,f
end

local function ClearChildren(frame)
    if not frame then return end
    if frame.GetChildren then
        local children={frame:GetChildren()}
        for _,child in ipairs(children) do child:Hide();child:SetParent(nil) end
    end
    if frame.GetRegions then
        local regions={frame:GetRegions()}
        for _,region in ipairs(regions) do if region.Hide then region:Hide() end end
    end
end

local function DateText(value)
    if not value then return "" end
    return date("%d %b %Y  %H:%M",value)
end

local function Confirm(title,message,confirmLabel,onConfirm)
    local C=Palette()
    local shade,f=Modal(title,540,260)
    local body=Text(f,message,10,C.muted);body:SetPoint("TOPLEFT",20,-70);body:SetWidth(500);body:SetWordWrap(true)
    local cancel=Button(f,"Cancel",92,32,false);cancel:SetPoint("BOTTOMLEFT",20,20);cancel:SetScript("OnClick",function()shade:Hide()end)
    local yes=Button(f,confirmLabel or "Confirm",112,32,true);yes:SetPoint("BOTTOMRIGHT",-20,20)
    yes.label:SetTextColor(C.red[1],C.red[2],C.red[3],1)
    yes:SetScript("OnClick",function()shade:Hide();if onConfirm then onConfirm() end end)
    shade._rpalConfirmButton=yes
    return shade
end

local function ArcFileCreator(onCreated)
    local C=Palette()
    local shade,f=Modal("NEW STORY ARC FILE",630,390)
    local intro=Text(f,"Create a separate story file. Its board can grow into as many events, paths and endings as your roleplay needs.",9.5,C.muted)
    intro:SetPoint("TOPLEFT",20,-58);intro:SetWidth(590);intro:SetWordWrap(true)
    local tl=Text(f,"STORY ARC NAME",8,C.muted);tl:SetPoint("TOPLEFT",20,-105)
    local title=EditBox(f,590,38,false);title:SetPoint("TOPLEFT",20,-124);title:SetAutoFocus(true)
    local nl=Text(f,"FILE NOTE  (OPTIONAL)",8,C.muted);nl:SetPoint("TOPLEFT",20,-178)
    local note=EditBox(f,590,112,true);note:SetPoint("TOPLEFT",20,-197)
    local create=Button(f,"Create & Open",126,34,true);create:SetPoint("BOTTOMRIGHT",-20,18)
    create:SetScript("OnClick",function()
        local ok,result=RPAL.CreateStoryArc and RPAL.CreateStoryArc(title:GetText(),note:GetText())
        if not ok then print("|cffff7777ODYN:|r "..tostring(result or "Could not create Story Arc."));return end
        shade:Hide()
        if onCreated then onCreated(result) end
    end)
    shade._rpalCreateButton=create
    return shade
end

local function StoryMusicTitle(musicID)
    if not musicID then return "No Story Arc music selected" end
    if TRP3_API and TRP3_API.utils and TRP3_API.utils.music and type(TRP3_API.utils.music.getTitle)=="function" then
        local ok,value=pcall(TRP3_API.utils.music.getTitle,musicID)
        if ok and value and tostring(value)~="" then return tostring(value) end
    end
    return "WoW music file #"..tostring(musicID)
end

local function StopStoryMusic()
    if TRP3_API and TRP3_API.utils and TRP3_API.utils.music and type(TRP3_API.utils.music.stopMusic)=="function" then
        pcall(TRP3_API.utils.music.stopMusic)
    elseif StopMusic then
        pcall(StopMusic)
    end
end

local function PlayStoryMusic(musicID)
    musicID=tonumber(musicID)
    if not musicID then return false end
    if TRP3_API and TRP3_API.utils and TRP3_API.utils.music and type(TRP3_API.utils.music.playMusic)=="function" then
        local ok=pcall(TRP3_API.utils.music.playMusic,musicID,"ODYN Story Arc")
        if ok then return true end
    end
    return PlayMusic and pcall(PlayMusic,musicID) or false
end

local function StoryMusicPicker(arc,onChanged)
    local callback=function(music)
        local musicID=tonumber(music)
        if not musicID then return end
        local name=StoryMusicTitle(musicID)
        RPAL.UpdateStoryArc(arc.id,{musicID=musicID,musicName=name})
        if onChanged then onChanged() end
    end
    if TRP3_API and TRP3_API.popup and TRP3_API.popup.MUSICS and type(TRP3_API.popup.showPopup)=="function" then
        local ok=pcall(TRP3_API.popup.showPopup,TRP3_API.popup.MUSICS,nil,{callback})
        if ok then
            local browser=TRP3_MusicBrowser
            if browser then
                browser:SetFrameStrata("TOOLTIP");browser:SetFrameLevel(700)
                if browser.SetToplevel then browser:SetToplevel(true) end
                if browser.Raise then browser:Raise() end
            end
            return true
        end
    end
    local C=Palette()
    local shade,f=Modal("SELECT STORY MUSIC",560,300)
    local hint=Text(f,"Total RP 3's music browser is not available. Enter a numeric WoW FileData ID; Watch will play that in-game music file.",9,C.muted)
    hint:SetPoint("TOPLEFT",20,-62);hint:SetWidth(520);hint:SetWordWrap(true)
    local label=Text(f,"WOW MUSIC FILE ID",8,C.muted);label:SetPoint("TOPLEFT",20,-125)
    local input=EditBox(f,520,38,false);input:SetPoint("TOPLEFT",20,-145);input:SetText(tostring(arc.musicID or ""));input:SetAutoFocus(true)
    local choose=Button(f,"Use Music",106,32,true);choose:SetPoint("BOTTOMRIGHT",-20,20)
    choose:SetScript("OnClick",function()
        local musicID=tonumber(input:GetText())
        if not musicID or musicID<=0 then print("|cffff7777ODYN:|r Enter a valid numeric WoW music FileData ID.");return end
        callback(math.floor(musicID));shade:Hide()
    end)
    return shade
end
RPAL.OpenStoryArcMusicPicker=StoryMusicPicker

local function StoryArcEditor(arc,onSaved)
    local C=Palette()
    local shade,f=Modal("EDIT STORY ARC FILE",630,470)
    local tl=Text(f,"STORY ARC NAME",8,C.muted);tl:SetPoint("TOPLEFT",20,-65)
    local title=EditBox(f,590,38,false);title:SetPoint("TOPLEFT",20,-84);title:SetText(arc.title or "")
    local nl=Text(f,"FILE NOTE",8,C.muted);nl:SetPoint("TOPLEFT",20,-138)
    local note=EditBox(f,590,112,true);note:SetPoint("TOPLEFT",20,-157);note:SetText(arc.note or "")
    local ml=Text(f,"WATCH SOUNDTRACK",8,C.muted);ml:SetPoint("TOPLEFT",20,-282)
    local music=Text(f,arc.musicName~="" and arc.musicName or StoryMusicTitle(arc.musicID),9.5,arc.musicID and C.gold or C.muted)
    music:SetPoint("TOPLEFT",20,-303);music:SetWidth(330);music:SetWordWrap(false)
    local chooseMusic=Button(f,"Choose Music",106,30,false);chooseMusic:SetPoint("TOPRIGHT",-20,-292)
    chooseMusic:SetScript("OnClick",function()StoryMusicPicker(arc,function()music:SetText(arc.musicName~="" and arc.musicName or StoryMusicTitle(arc.musicID));music:SetTextColor(C.gold[1],C.gold[2],C.gold[3],1)end)end)
    local preview=Button(f,"Preview",78,28,false);preview:SetPoint("TOPLEFT",20,-338);preview:SetScript("OnClick",function()if arc.musicID then PlayStoryMusic(arc.musicID) end end)
    local clear=Button(f,"Remove",78,28,false);clear:SetPoint("LEFT",preview,"RIGHT",7,0);clear:SetScript("OnClick",function()StopStoryMusic();RPAL.UpdateStoryArc(arc.id,{musicID=false,musicName=""});music:SetText("No Story Arc music selected");music:SetTextColor(C.muted[1],C.muted[2],C.muted[3],1)end)
    local save=Button(f,"Save File",104,34,true);save:SetPoint("BOTTOMRIGHT",-20,18)
    save:SetScript("OnClick",function()
        local ok,err=RPAL.UpdateStoryArc(arc.id,{title=title:GetText(),note=note:GetText()})
        if not ok then print("|cffff7777ODYN:|r "..tostring(err));return end
        shade:Hide();if onSaved then onSaved() end
    end)
    shade:HookScript("OnHide",function()StopStoryMusic()end)
    return shade
end

local function StoryArcTransfer(arc,onImported)
    local C=Palette()
    local shade,f=Modal("SHARE STORY ARC — EXPORT / IMPORT",760,620)
    local hintText=arc and "Copy this compact ODYN Story Arc code to share the complete board, or paste another compatible Story Arc export and import it as a new, separate file."
        or "Paste a compatible ODYN Story Arc export below. It will be validated and imported as a new, separate Story Arc file."
    local hint=Text(f,hintText,9,C.muted)
    hint:SetPoint("TOPLEFT",20,-58);hint:SetWidth(720);hint:SetWordWrap(true)
    local payload=EditBox(f,720,430,true);payload:SetPoint("TOPLEFT",20,-96)
    local exported,err
    if arc and RPAL.ExportStoryArc then exported,err=RPAL.ExportStoryArc(arc) end
    payload:SetText(exported or "");payload:SetAutoFocus(true)
    if payload.HighlightText then payload:HighlightText() end
    if arc and not exported and err then print("|cffff7777ODYN:|r "..tostring(err)) end
    local export=Button(f,"Reload Export",110,32,false);export:SetPoint("BOTTOMLEFT",20,20)
    export:SetShown(arc and true or false)
    export:SetScript("OnClick",function()
        local text=(arc and RPAL.ExportStoryArc) and RPAL.ExportStoryArc(arc) or ""
        payload:SetText(text or "");payload:SetFocus();if payload.HighlightText then payload:HighlightText() end
    end)
    local import=Button(f,"Import as New File",142,32,true);import:SetPoint("BOTTOMRIGHT",-20,20)
    import:SetScript("OnClick",function()
        local ok,result=RPAL.ImportStoryArc and RPAL.ImportStoryArc(payload:GetText())
        if not ok then print("|cffff7777ODYN:|r "..tostring(result or "Story Arc import failed."));return end
        shade:Hide();if onImported then onImported(result) end
    end)
    shade._rpalTransferEdit=payload;shade._rpalImportArcButton=import
    return shade
end
RPAL.OpenStoryArcTransfer=StoryArcTransfer

-- ------------------------------------------------------------
-- Watch presentation
-- ------------------------------------------------------------

function RPAL.OpenStoryArcWatch(arcOrID,startStepID)
    local C=Palette()
    local arc=type(arcOrID)=="table" and RPAL.NormalizeStoryArc(arcOrID) or RPAL.GetStoryArc(arcOrID)
    if not arc or #arc.stepOrder==0 then
        print("|cffff7777ODYN:|r Add at least one event before watching this Story Arc.")
        return nil
    end
    if activeWatchShade and activeWatchShade:IsShown() then activeWatchShade:Hide() end

    storyModalSerial=storyModalSerial+1
    local shade=CreateFrame("Frame","RPAL_StoryWatch_"..tostring(storyModalSerial),UIParent,"BackdropTemplate")
    activeWatchShade=shade
    shade:SetAllPoints();shade:SetFrameStrata("FULLSCREEN_DIALOG");shade:SetFrameLevel(9920)
    if shade.SetToplevel then shade:SetToplevel(true) end
    shade:EnableMouse(true);shade:SetBackdrop({bgFile=WHITE});shade:SetBackdropColor(0.006,0.004,0.012,0.97)
    shade._rpalStoryArcWatch=true;shade._rpalArcID=arc.id;shade._rpalHistory={};shade._rpalPlaying=true;shade._rpalToken=0
    if RPAL.RegisterEscapeFrame then RPAL.RegisterEscapeFrame(shade) end
    shade._rpalEscapeFailsafe=true
    if arc.musicID then shade._rpalStoryMusicPlaying=PlayStoryMusic(arc.musicID) and true or false end

    local topGlow=shade:CreateTexture(nil,"BACKGROUND")
    topGlow:SetTexture(SPARK);topGlow:SetBlendMode("ADD");topGlow:SetSize(900,310);topGlow:SetPoint("TOP",0,90)
    topGlow:SetVertexColor(C.gold[1],C.gold[2],C.gold[3],0.13)
    local bottomGlow=shade:CreateTexture(nil,"BACKGROUND")
    bottomGlow:SetTexture(SPARK);bottomGlow:SetBlendMode("ADD");bottomGlow:SetSize(760,250);bottomGlow:SetPoint("BOTTOM",0,-70)
    bottomGlow:SetVertexColor(C.blue[1],C.blue[2],C.blue[3],0.08)

    local theater=CreateFrame("Frame",nil,shade,"BackdropTemplate")
    theater:SetSize(980,620);theater:SetPoint("CENTER");Backdrop(theater,C.bg,C.gold,20)
    local arcLabel=Text(theater,"STORY ARC  •  WATCH",8,C.gold);arcLabel:SetPoint("TOPLEFT",28,-22)
    local arcTitle=Text(theater,arc.title,14,C.pale,DISPLAY);arcTitle:SetPoint("TOPLEFT",28,-40);arcTitle:SetWidth(740);arcTitle:SetWordWrap(false)
    local close=Button(theater,"Exit",72,28,false);close:SetPoint("TOPRIGHT",-20,-18);close:SetScript("OnClick",function()shade:Hide()end)

    local rule=theater:CreateTexture(nil,"ARTWORK");rule:SetTexture(WHITE);rule:SetHeight(1)
    rule:SetPoint("TOPLEFT",28,-72);rule:SetPoint("TOPRIGHT",-28,-72);rule:SetVertexColor(C.gold[1],C.gold[2],C.gold[3],0.32)

    local stage=CreateFrame("Frame",nil,theater,"BackdropTemplate")
    stage:SetSize(900,390);stage:SetPoint("TOP",0,-92);Backdrop(stage,C.panel,C.border,18)
    local stageGlow=stage:CreateTexture(nil,"BACKGROUND",nil,-1);stageGlow:SetTexture(SPARK);stageGlow:SetBlendMode("ADD")
    stageGlow:SetSize(650,270);stageGlow:SetPoint("CENTER",0,10);stageGlow:SetVertexColor(C.gold[1],C.gold[2],C.gold[3],0.10)

    local chapter=Text(stage,"",8,C.muted);chapter:SetPoint("TOP",0,-28)
    local location=Text(stage,"",9,C.gold);location:SetPoint("TOP",0,-51)
    local title=Text(stage,"",27,C.pale,DISPLAY);title:SetPoint("TOP",0,-80);title:SetWidth(780);title:SetJustifyH("CENTER");title:SetWordWrap(true)
    local note=Text(stage,"",13,C.pale,DISPLAY);note:SetPoint("TOP",0,-145);note:SetWidth(760);note:SetJustifyH("CENTER");note:SetWordWrap(true)
    local people=Text(stage,"",9,C.muted);people:SetPoint("BOTTOM",0,57);people:SetWidth(760);people:SetJustifyH("CENTER");people:SetWordWrap(false)
    local status=Text(stage,"",8,C.gold);status:SetPoint("BOTTOM",0,30)

    local branchHolder=CreateFrame("Frame",nil,theater)
    branchHolder:SetSize(860,82);branchHolder:SetPoint("BOTTOM",0,54)
    local prompt=Text(theater,"",9,C.gold);prompt:SetPoint("BOTTOM",0,142);prompt:SetWidth(850);prompt:SetJustifyH("CENTER")

    local back=Button(theater,"← Back",88,30,false);back:SetPoint("BOTTOMLEFT",28,18)
    local play=Button(theater,"Pause",80,30,false);play:SetPoint("BOTTOM",0,18)
    local nextButton=Button(theater,"Continue →",108,30,true);nextButton:SetPoint("BOTTOMRIGHT",-28,18)
    shade._rpalBackButton=back;shade._rpalPlayButton=play;shade._rpalNextButton=nextButton

    local function FadeStage()
        stage:SetAlpha(0);stage:SetScale(0.985)
        stage._rpalFadeAge=0
        stage:SetScript("OnUpdate",function(self,elapsed)
            self._rpalFadeAge=(self._rpalFadeAge or 0)+(elapsed or 0)
            local k=math.min(1,self._rpalFadeAge/0.32)
            local eased=1-((1-k)*(1-k))
            self:SetAlpha(eased);self:SetScale(0.985+(0.015*eased))
            if k>=1 then self:SetScript("OnUpdate",nil) end
        end)
    end

    local ShowStep
    local function Choose(link)
        if not link then return end
        ShowStep(link.to,true)
    end

    local function RenderBranches(outgoing)
        ClearChildren(branchHolder)
        shade._rpalBranchChoices={}
        if #outgoing<=1 then return end
        prompt:SetText("This event splits the story. Which path do you want to follow?")
        local count=math.min(6,#outgoing)
        local cols=math.min(3,count)
        local width=math.floor((850-((cols-1)*8))/cols)
        for index=1,count do
            local link=outgoing[index]
            local target=arc.steps[link.to]
            local col=(index-1)%cols
            local row=math.floor((index-1)/cols)
            local b=Button(branchHolder,tostring(target and target.title or "Unknown path"),width,32,true)
            b:SetPoint("TOPLEFT",col*(width+8),-(row*39))
            b:SetScript("OnClick",function()Choose(link)end)
            shade._rpalBranchChoices[#shade._rpalBranchChoices+1]=b
        end
    end

    ShowStep=function(stepID,pushHistory)
        local step=arc.steps[tostring(stepID or "")]
        if not step then return end
        if pushHistory and shade._rpalCurrentStepID then
            shade._rpalHistory[#shade._rpalHistory+1]=shade._rpalCurrentStepID
        end
        shade._rpalCurrentStepID=step.id
        shade._rpalToken=(shade._rpalToken or 0)+1
        local token=shade._rpalToken
        local index=1
        for i,id in ipairs(arc.stepOrder) do if id==step.id then index=i break end end
        local outgoing=RPAL.GetStoryArcOutgoing(arc,step.id)
        local bondNames=RPAL.GetStoryArcBondNames and RPAL.GetStoryArcBondNames(step) or {}

        chapter:SetText("EVENT "..string.format("%02d",index).."  •  "..tostring(#arc.stepOrder).." IN FILE")
        location:SetText(step.location~="" and ("◆  "..string.upper(step.location).."  ◆") or "◆  LOCATION NOT RECORDED  ◆")
        title:SetText(step.title or "Untitled Event")
        note:SetText((step.note and step.note~="") and step.note or "No note was written for this event.")
        people:SetText(#bondNames>0 and ("WITH  •  "..table.concat(bondNames,"  •  ")) or "No Bond characters linked to this event")
        status:SetText(#outgoing==0 and "ENDING REACHED" or (#outgoing>1 and ("BRANCH POINT  •  "..tostring(#outgoing).." PATHS") or "THE STORY CONTINUES"))
        back:SetShown(#shade._rpalHistory>0)
        prompt:SetText("")
        RenderBranches(outgoing)
        nextButton:SetShown(#outgoing<=1)
        nextButton.label:SetText(#outgoing==0 and "Replay" or "Continue →")
        shade._rpalOutgoing=outgoing
        FadeStage()

        if #outgoing==1 and shade._rpalPlaying and C_Timer and C_Timer.After then
            C_Timer.After(5.2,function()
                if shade:IsShown() and shade._rpalPlaying and shade._rpalToken==token and shade._rpalCurrentStepID==step.id then
                    Choose(outgoing[1])
                end
            end)
        end
    end
    shade._rpalShowStep=ShowStep

    back:SetScript("OnClick",function()
        local previous=table.remove(shade._rpalHistory)
        if previous then ShowStep(previous,false) end
    end)
    play:SetScript("OnClick",function()
        shade._rpalPlaying=not shade._rpalPlaying
        play.label:SetText(shade._rpalPlaying and "Pause" or "Play")
        shade._rpalToken=(shade._rpalToken or 0)+1
        if shade._rpalPlaying then ShowStep(shade._rpalCurrentStepID,false) end
    end)
    nextButton:SetScript("OnClick",function()
        local outgoing=shade._rpalOutgoing or {}
        if #outgoing==0 then
            shade._rpalHistory={}
            ShowStep(arc.rootStepID or arc.stepOrder[1],false)
        elseif #outgoing==1 then
            Choose(outgoing[1])
        end
    end)

    shade:HookScript("OnHide",function(self)
        self._rpalToken=(self._rpalToken or 0)+1
        if self._rpalStoryMusicPlaying then StopStoryMusic();self._rpalStoryMusicPlaying=false end
        if activeWatchShade==self then activeWatchShade=nil end
    end)
    ShowStep(startStepID or arc.rootStepID or arc.stepOrder[1],false)
    return shade,theater
end

-- ------------------------------------------------------------
-- Board
-- ------------------------------------------------------------

local function BondPicker(step,onSaved,titleText,introText)
    local C=Palette()
    local shade,f=Modal(titleText or "LINK CHARACTERS FROM BONDS",620,560)
    local intro=Text(f,introText or "Choose the characters who were part of this event. Names come from Bonds and keep their TRP display names.",9,C.muted)
    intro:SetPoint("TOPLEFT",20,-58);intro:SetWidth(580);intro:SetWordWrap(true)
    local selected={}
    for _,key in ipairs(step.bondKeys or {}) do selected[tostring(key):lower()]=true end
    local bonds=RPAL.GetBonds and RPAL.GetBonds() or {}

    local searchLabel=Text(f,"SEARCH BONDS",7,C.muted);searchLabel:SetPoint("TOPLEFT",20,-101)
    local search=EditBox(f,430,30,false);search:SetPoint("TOPLEFT",20,-115)
    local matchCount=Text(f,"",7.5,C.muted);matchCount:SetPoint("RIGHT",f,"RIGHT",-24,0);matchCount:SetPoint("TOP",f,"TOP",0,-125)
    matchCount:SetWidth(130);matchCount:SetJustifyH("RIGHT")

    local scroll=CreateFrame("ScrollFrame",nil,f,"UIPanelScrollFrameTemplate")
    if RPAL.SkinScrollFrame then RPAL.SkinScrollFrame(scroll) end
    scroll:SetPoint("TOPLEFT",20,-153);scroll:SetPoint("BOTTOMRIGHT",-38,70)
    local content=CreateFrame("Frame",nil,scroll);content:SetWidth(552);content:SetHeight(1);scroll:SetScrollChild(content)
    local empty=Text(content,"No Bonds yet. Characters appear here after tracked Sessions build a Bond.",10,C.muted)
    empty:SetPoint("TOPLEFT",8,-20);empty:SetWidth(520);empty:SetWordWrap(true)
    local rows={}
    for _,bond in ipairs(bonds) do
        local key=tostring(bond.key or bond.name or ""):lower()
        local row=CreateFrame("Button",nil,content,"BackdropTemplate");row:SetSize(268,36)
        Backdrop(row,C.panel2,C.border)
        local check=Text(row,selected[key] and "✓" or "○",12,selected[key] and C.green or C.muted);check:SetPoint("LEFT",9,0)
        local name=Text(row,bond.name or key,9.2,C.pale,DISPLAY);name:SetPoint("TOPLEFT",31,-6);name:SetWidth(224);name:SetWordWrap(false)
        local relation=Text(row,string.upper(bond.relationship or "Acquaintance"),6.8,C.gold);relation:SetPoint("TOPLEFT",31,-22);relation:SetWidth(224);relation:SetWordWrap(false)
        row:SetScript("OnClick",function()
            selected[key]=not selected[key]
            check:SetText(selected[key] and "✓" or "○")
            check:SetTextColor(unpack(selected[key] and C.green or C.muted))
        end)
        row._rpalBond=bond;row._rpalBondKey=key
        rows[#rows+1]=row
    end
    local function Matches(bond,query)
        if query=="" then return true end
        local text=table.concat({bond.name or "",bond.trpName or "",bond.relationship or "",bond.trpRelationship or "",bond.note or ""}," "):lower()
        return text:find(query,1,true)~=nil
    end
    local function ApplySearch()
        local query=tostring(search:GetText() or ""):lower():gsub("^%s+",""):gsub("%s+$","")
        local shown=0
        for _,row in ipairs(rows) do
            local visible=Matches(row._rpalBond or {},query)
            row:SetShown(visible)
            if visible then
                local col=shown%2;local line=math.floor(shown/2)
                row:ClearAllPoints();row:SetPoint("TOPLEFT",col*276,-(line*42))
                shown=shown+1
            end
        end
        local lines=math.ceil(shown/2)
        content:SetHeight(math.max(1,lines*42))
        empty:SetShown(#bonds==0 or shown==0)
        empty:SetText(#bonds==0 and "No Bonds yet. Characters appear here after tracked Sessions build a Bond." or "No Bonds match that search.")
        matchCount:SetText(tostring(shown).." / "..tostring(#bonds))
        if scroll.SetVerticalScroll then scroll:SetVerticalScroll(0) end
    end
    search:SetScript("OnTextChanged",ApplySearch)
    search:SetScript("OnEscapePressed",function(self)self:SetText("");self:ClearFocus()end)
    search:SetScript("OnEnterPressed",function(self)self:ClearFocus()end)
    shade._rpalBondSearch=search;shade._rpalBondRows=rows;shade._rpalApplyBondSearch=ApplySearch
    ApplySearch()
    local save=Button(f,"Save Characters",128,34,true);save:SetPoint("BOTTOMRIGHT",-20,18)
    save:SetScript("OnClick",function()
        local keys={}
        for key,wanted in pairs(selected) do if wanted then keys[#keys+1]=key end end
        table.sort(keys)
        shade:Hide();if onSaved then onSaved(keys) end
    end)
    return shade
end

local function DrawLine(canvas,x1,y1,x2,y2,color,alpha)
    local dx,dy=x2-x1,y2-y1
    local length=math.sqrt((dx*dx)+(dy*dy))
    if length<1 then return nil end
    -- Connections must sit above the canvas backdrop.  BACKGROUND textures can
    -- be hidden by BackdropTemplate's own opaque background on live clients.
    local line=canvas:CreateTexture(nil,"ARTWORK",nil,-2)
    line:SetTexture(WHITE);line:SetSize(length,2.2)
    line:SetPoint("CENTER",canvas,"TOPLEFT",(x1+x2)/2,-((y1+y2)/2))
    local angle
    if math.atan2 then angle=math.atan2(dy,dx)
    elseif dx==0 then angle=(dy>=0 and math.pi/2 or -math.pi/2)
    else
        angle=math.atan(dy/dx)
        if dx<0 then angle=angle+math.pi end
    end
    if line.SetRotation then line:SetRotation(-angle) end
    line:SetVertexColor(color[1],color[2],color[3],alpha or 0.72)
    return line
end

function RPAL.OpenStoryArcBoard(arcOrID,options)
    local C=Palette()
    options=options or {}
    local readOnly=options.readOnly and true or false
    local arc=type(arcOrID)=="table" and RPAL.NormalizeStoryArc(arcOrID) or RPAL.GetStoryArc(arcOrID)
    if not arc then return nil end
    if activeBoardShade and activeBoardShade:IsShown() then activeBoardShade:Hide() end

    storyModalSerial=storyModalSerial+1
    local shade=CreateFrame("Frame","RPAL_StoryBoard_"..tostring(storyModalSerial),UIParent,"BackdropTemplate")
    activeBoardShade=shade
    shade:SetAllPoints();shade:SetFrameStrata("FULLSCREEN_DIALOG");shade:SetFrameLevel(9880)
    if shade.SetToplevel then shade:SetToplevel(true) end
    shade:EnableMouse(true);shade:SetBackdrop({bgFile=WHITE});shade:SetBackdropColor(0,0,0,0.78)
    shade._rpalStoryBoard=true;shade._rpalArcID=arc.id;shade._rpalReadOnly=readOnly
    if RPAL.RegisterEscapeFrame then RPAL.RegisterEscapeFrame(shade) end
    shade._rpalEscapeFailsafe=true

    local f=CreateFrame("Frame",nil,shade,"BackdropTemplate")
    f:SetSize(1180,780);f:SetPoint("CENTER");f:SetFrameLevel(9890);Backdrop(f,C.bg,C.gold,20)
    if f.SetToplevel then f:SetToplevel(true) end
    shade._rpalBoardFrame=f

    local title=Text(f,arc.title,18,C.gold,DISPLAY);title:SetPoint("TOPLEFT",20,-17);title:SetWidth(410);title:SetWordWrap(false)
    local readOnlySubtitle=arc.truncated and "PINNED STORY  •  READ ONLY BOARD  •  PARTIAL PUBLIC COPY" or "PINNED STORY  •  READ ONLY BOARD"
    local subtitle=Text(f,readOnly and readOnlySubtitle or "STORY BOARD  •  DRAG EVENTS  •  WHEEL ZOOM  •  RIGHT-DRAG PAN",7.5,C.muted);subtitle:SetPoint("TOPLEFT",21,-41)
    local close=Button(f,"Close",70,30,false);close:SetPoint("TOPRIGHT",-12,-15);close:SetScript("OnClick",function()shade:Hide()end)
    local edit=Button(f,"Edit File",82,30,false);edit:SetPoint("RIGHT",close,"LEFT",-7,0)
    local pin=Button(f,arc.pinned and "Unpin" or "Pin",60,30,false);pin:SetPoint("RIGHT",edit,"LEFT",-7,0)
    local export=Button(f,"Export",66,30,false);export:SetPoint("RIGHT",pin,"LEFT",-7,0)
    local share=Button(f,"Share",66,30,false);share:SetPoint("RIGHT",export,"LEFT",-7,0)
    local watch=Button(f,"▶ Watch",88,30,true);watch:SetPoint("RIGHT",share,"LEFT",-7,0)
    local cast=Button(f,"Bonds "..tostring(#(arc.bondKeys or {})),70,30,false);cast:SetPoint("RIGHT",watch,"LEFT",-7,0)
    local add=Button(f,"+ Event",82,30,true);add:SetPoint("RIGHT",cast,"LEFT",-7,0)
    shade._rpalAddStepButton=add;shade._rpalWatchButton=watch;shade._rpalPinButton=pin;shade._rpalShareButton=share;shade._rpalExportButton=export;shade._rpalArcBondsButton=cast
    add:SetShown(not readOnly);cast:SetShown(not readOnly);pin:SetShown(not readOnly);edit:SetShown(not readOnly);share:SetShown(not readOnly);export:SetShown(not readOnly)
    if readOnly then watch:ClearAllPoints();watch:SetPoint("RIGHT",close,"LEFT",-7,0) end

    local status=Text(f,readOnly and "Read-only pinned story. Select events, wheel to zoom, right-drag to pan, or press Watch." or "Changes auto-save. Wheel to zoom; right-drag the empty board to pan.",8,C.muted)
    status:SetPoint("BOTTOMLEFT",20,14);status:SetWidth(790);status:SetWordWrap(false)
    shade._rpalBoardStatus=status

    local viewport=CreateFrame("Frame",nil,f,"BackdropTemplate")
    viewport:SetSize(840,680);viewport:SetPoint("TOPLEFT",20,-66);Backdrop(viewport,{C.panel[1],C.panel[2],C.panel[3],0.95},C.border)
    if viewport.SetClipsChildren then viewport:SetClipsChildren(true) end
    viewport:EnableMouse(true);if viewport.EnableMouseWheel then viewport:EnableMouseWheel(true) end
    shade._rpalViewport=viewport

    local inspector=CreateFrame("Frame",nil,f,"BackdropTemplate")
    inspector:SetSize(292,680);inspector:SetPoint("TOPRIGHT",-20,-66);Backdrop(inspector,C.panel,C.border)
    shade._rpalInspector=inspector

    local canvas=CreateFrame("Frame",nil,viewport,"BackdropTemplate")
    canvas:SetSize(BOARD_W,BOARD_H);canvas:EnableMouse(true);canvas:SetMovable(true)
    Backdrop(canvas,{C.panel[1],C.panel[2],C.panel[3],0.995},{C.border[1],C.border[2],C.border[3],0.55},10)
    if canvas.EnableMouseWheel then canvas:EnableMouseWheel(true) end
    canvas:RegisterForDrag("RightButton")
    shade._rpalCanvas=canvas
    shade._rpalNodeFrames={};shade._rpalLineObjects={};shade._rpalSelectedStepID=arc.rootStepID
    shade._rpalViewX=tonumber(arc.view and arc.view.x) or 0
    shade._rpalViewY=tonumber(arc.view and arc.view.y) or 0
    shade._rpalZoom=tonumber(arc.view and arc.view.zoom) or 1

    local function ClampView()
        local zoom=shade._rpalZoom or 1
        local vw=(viewport.GetWidth and viewport:GetWidth()) or 840
        local vh=(viewport.GetHeight and viewport:GetHeight()) or 680
        local minX=math.min(0,vw-(BOARD_W*zoom));local maxY=math.max(0,(BOARD_H*zoom)-vh)
        shade._rpalViewX=math.max(minX,math.min(0,tonumber(shade._rpalViewX) or 0))
        shade._rpalViewY=math.max(0,math.min(maxY,tonumber(shade._rpalViewY) or 0))
        shade._rpalPanBounds={minX=minX,maxX=0,minY=0,maxY=maxY}
    end
    local function ApplyView()
        ClampView()
        canvas:SetScale(shade._rpalZoom or 1)
        canvas:ClearAllPoints();canvas:SetPoint("TOPLEFT",viewport,"TOPLEFT",shade._rpalViewX or 0,shade._rpalViewY or 0)
    end
    shade._rpalApplyView=ApplyView

    -- Drafting-paper grid.  BORDER is deliberate: BACKGROUND sublevels render
    -- behind BackdropTemplate on live clients, which made the grid disappear.
    shade._rpalGridLayer="BORDER";shade._rpalGridLineCount=0
    for x=0,BOARD_W,100 do
        local line=canvas:CreateTexture(nil,"BORDER",nil,-7);line:SetTexture(WHITE);line:SetSize(x%500==0 and 2 or 1,BOARD_H)
        line:SetPoint("TOPLEFT",x,0);line:SetVertexColor(C.gold[1],C.gold[2],C.gold[3],x%500==0 and 0.18 or 0.065)
        shade._rpalGridLineCount=shade._rpalGridLineCount+1
    end
    for y=0,BOARD_H,100 do
        local line=canvas:CreateTexture(nil,"BORDER",nil,-7);line:SetTexture(WHITE);line:SetSize(BOARD_W,y%500==0 and 2 or 1)
        line:SetPoint("TOPLEFT",0,-y);line:SetVertexColor(C.gold[1],C.gold[2],C.gold[3],y%500==0 and 0.18 or 0.065)
        shade._rpalGridLineCount=shade._rpalGridLineCount+1
    end

    canvas:SetScript("OnDragStart",function()
        if not GetCursorPosition then return end
        local x,y=GetCursorPosition()
        canvas._panStartX=x;canvas._panStartY=y
        canvas._panOriginX=shade._rpalViewX or 0;canvas._panOriginY=shade._rpalViewY or 0
        canvas:SetScript("OnUpdate",function()
            local cx,cy=GetCursorPosition()
            local scale=(viewport.GetEffectiveScale and viewport:GetEffectiveScale()) or ((UIParent.GetEffectiveScale and UIParent:GetEffectiveScale()) or 1)
            shade._rpalViewX=canvas._panOriginX+((cx-canvas._panStartX)/scale)
            shade._rpalViewY=canvas._panOriginY+((cy-canvas._panStartY)/scale)
            ApplyView()
        end)
    end)
    canvas:SetScript("OnDragStop",function()
        canvas:SetScript("OnUpdate",nil)
        if not readOnly then RPAL.SetStoryArcView(arc.id,shade._rpalViewX,shade._rpalViewY,shade._rpalZoom) end
    end)

    local zoomOut=Button(viewport,"−",28,26,false);zoomOut:SetPoint("BOTTOMLEFT",8,8)
    local fit=Button(viewport,"Fit",42,26,false);fit:SetPoint("LEFT",zoomOut,"RIGHT",5,0)
    local zoomIn=Button(viewport,"+",28,26,false);zoomIn:SetPoint("LEFT",fit,"RIGHT",5,0)
    local zoomLabel=Text(viewport,tostring(math.floor(shade._rpalZoom*100)).."%",7,C.muted);zoomLabel:SetPoint("LEFT",zoomIn,"RIGHT",7,0)
    local cancelConnect=Button(viewport,"Cancel Link",88,26,false);cancelConnect:SetPoint("BOTTOMRIGHT",-8,8);cancelConnect:SetShown(false)
    shade._rpalCancelConnectButton=cancelConnect
    local function SetZoom(value,anchorX,anchorY)
        local oldZoom=shade._rpalZoom or 1
        local newZoom=math.max(0.28,math.min(1.35,tonumber(value) or oldZoom))
        local vw=(viewport.GetWidth and viewport:GetWidth()) or 840
        local vh=(viewport.GetHeight and viewport:GetHeight()) or 680
        if anchorX==nil or anchorY==nil then anchorX,anchorY=vw/2,vh/2 end
        anchorX=math.max(0,math.min(vw,anchorX));anchorY=math.max(0,math.min(vh,anchorY))
        local boardX=(anchorX-(shade._rpalViewX or 0))/oldZoom
        local boardY=(anchorY+(shade._rpalViewY or 0))/oldZoom
        shade._rpalZoom=newZoom
        shade._rpalViewX=anchorX-(boardX*newZoom)
        shade._rpalViewY=(boardY*newZoom)-anchorY
        zoomLabel:SetText(tostring(math.floor(shade._rpalZoom*100)).."%")
        ApplyView()
        if not readOnly then RPAL.SetStoryArcView(arc.id,shade._rpalViewX,shade._rpalViewY,shade._rpalZoom) end
    end
    zoomOut:SetScript("OnClick",function()SetZoom((shade._rpalZoom or 1)-0.1)end)
    zoomIn:SetScript("OnClick",function()SetZoom((shade._rpalZoom or 1)+0.1)end)
    fit:SetScript("OnClick",function()
        local minX,minY,maxX,maxY
        for _,stepID in ipairs(arc.stepOrder) do
            local step=arc.steps[stepID]
            minX=math.min(minX or step.x,step.x);minY=math.min(minY or step.y,step.y)
            maxX=math.max(maxX or (step.x+NODE_W),step.x+NODE_W);maxY=math.max(maxY or (step.y+NODE_H),step.y+NODE_H)
        end
        if not minX then return end
        local vw=(viewport.GetWidth and viewport:GetWidth()) or 840
        local vh=(viewport.GetHeight and viewport:GetHeight()) or 680
        local zoom=math.min(1.15,(vw-100)/math.max(1,maxX-minX),(vh-100)/math.max(1,maxY-minY))
        shade._rpalZoom=math.max(0.28,zoom)
        shade._rpalViewX=50-(minX*shade._rpalZoom)
        shade._rpalViewY=(minY*shade._rpalZoom)-30
        zoomLabel:SetText(tostring(math.floor(shade._rpalZoom*100)).."%")
        ApplyView()
        if not readOnly then RPAL.SetStoryArcView(arc.id,shade._rpalViewX,shade._rpalViewY,shade._rpalZoom) end
    end)
    local function WheelZoom(_,delta)
        local vw=(viewport.GetWidth and viewport:GetWidth()) or 840
        local vh=(viewport.GetHeight and viewport:GetHeight()) or 680
        local anchorX,anchorY=vw/2,vh/2
        if GetCursorPosition and viewport.GetLeft and viewport.GetTop then
            local cx,cy=GetCursorPosition()
            local scale=(UIParent.GetEffectiveScale and UIParent:GetEffectiveScale()) or 1
            local left,top=viewport:GetLeft(),viewport:GetTop()
            if left and top then anchorX=(cx/scale)-left;anchorY=top-(cy/scale) end
        end
        SetZoom((shade._rpalZoom or 1)+((tonumber(delta) or 0)>0 and 0.08 or -0.08),anchorX,anchorY)
    end
    viewport:SetScript("OnMouseWheel",WheelZoom);canvas:SetScript("OnMouseWheel",WheelZoom)
    shade._rpalSetZoom=SetZoom

    local RefreshBoard,RefreshInspector,CommitInspector

    local function StartConnect(stepID)
        if readOnly then return end
        shade._rpalConnectSource=stepID
        status:SetText("CONNECT MODE: choose the destination event. Click the source again or press Cancel to stop.")
        shade._rpalConnectMode=true
        cancelConnect:SetShown(true)
        RefreshBoard()
    end

    local function SelectStep(stepID)
        if CommitInspector then CommitInspector("selection") end
        if readOnly then
            shade._rpalSelectedStepID=stepID
            RefreshBoard()
            return
        end
        if shade._rpalConnectSource then
            if shade._rpalConnectSource==stepID then
                shade._rpalConnectSource=nil;shade._rpalConnectMode=false
                cancelConnect:SetShown(false)
                status:SetText("Connection cancelled. Select an event to edit it.")
            else
                local ok,err=RPAL.ConnectStoryArcSteps(arc.id,shade._rpalConnectSource,stepID)
                if not ok then print("|cffff7777ODYN:|r "..tostring(err)) end
                shade._rpalConnectSource=nil;shade._rpalConnectMode=false
                cancelConnect:SetShown(false)
                status:SetText(ok and "Events connected. Drag nodes anytime to keep the board readable." or "Connection was not created.")
            end
        end
        shade._rpalSelectedStepID=stepID
        RefreshBoard()
    end
    cancelConnect:SetScript("OnClick",function()
        if CommitInspector then CommitInspector("button") end
        shade._rpalConnectSource=nil;shade._rpalConnectMode=false;cancelConnect:SetShown(false)
        status:SetText("Connection cancelled. Select an event to edit it.")
        RefreshBoard()
    end)

    local function RefreshLines()
        for _,obj in ipairs(shade._rpalLineObjects or {}) do if obj.Hide then obj:Hide() end end
        shade._rpalLineObjects={}
        for _,link in ipairs(arc.links or {}) do
            local from,to=arc.steps[link.from],arc.steps[link.to]
            if from and to then
                local selected=shade._rpalSelectedStepID==from.id or shade._rpalSelectedStepID==to.id
                local color=selected and C.gold or C.muted
                local line=DrawLine(canvas,from.x+NODE_W,from.y+(NODE_H/2),to.x,to.y+(NODE_H/2),color,selected and 0.92 or 0.45)
                if line then shade._rpalLineObjects[#shade._rpalLineObjects+1]=line end
                local arrow=Text(canvas,"◆",7,color);arrow:SetPoint("CENTER",canvas,"TOPLEFT",to.x-5,-(to.y+(NODE_H/2)))
                shade._rpalLineObjects[#shade._rpalLineObjects+1]=arrow
            end
        end
        shade._rpalLinkCount=#arc.links
    end
    shade._rpalRefreshLines=RefreshLines

    local function BuildNode(step,index)
        local selected=shade._rpalSelectedStepID==step.id
        local source=shade._rpalConnectSource==step.id
        local outgoing=RPAL.GetStoryArcOutgoing(arc,step.id)
        local incoming=RPAL.GetStoryArcIncoming(arc,step.id)
        local border=source and C.blue or (selected and C.gold or C.border)
        local node=CreateFrame("Button",nil,canvas,"BackdropTemplate")
        node:SetSize(NODE_W,NODE_H);node:SetPoint("TOPLEFT",step.x,-step.y);node:SetMovable(true)
        node:EnableMouse(true);node:RegisterForClicks("LeftButtonUp")
        if node.EnableMouseWheel then node:EnableMouseWheel(true);node:SetScript("OnMouseWheel",WheelZoom) end
        if not readOnly then node:RegisterForDrag("LeftButton") end
        Backdrop(node,C.panel2,border,16)
        node._rpalStepID=step.id;shade._rpalNodeFrames[step.id]=node

        local bar=node:CreateTexture(nil,"ARTWORK");bar:SetTexture(WHITE);bar:SetPoint("TOPLEFT",2,-3);bar:SetPoint("BOTTOMLEFT",2,3);bar:SetWidth(4)
        local barColor=step.completed and C.green or (arc.rootStepID==step.id and C.gold or C.blue)
        bar:SetVertexColor(barColor[1],barColor[2],barColor[3],0.95)
        local kind=arc.rootStepID==step.id and "BEGINNING" or (#outgoing==0 and "ENDING" or (#outgoing>1 and "BRANCH" or ("EVENT "..string.format("%02d",index))))
        local eyebrow=Text(node,kind,6.8,barColor);eyebrow:SetPoint("TOPLEFT",12,-9)
        local name=Text(node,step.title,10.2,C.pale,DISPLAY);name:SetPoint("TOPLEFT",12,-24);name:SetWidth(151);name:SetWordWrap(false)
        local bondNames=RPAL.GetStoryArcBondNames and RPAL.GetStoryArcBondNames(step) or {}
        local bondLine=Text(node,#bondNames>0 and table.concat(bondNames,"  •  ") or "",6.8,C.blue)
        bondLine:SetPoint("TOPLEFT",12,-43);bondLine:SetWidth(153);bondLine:SetHeight(27);bondLine:SetWordWrap(true)
        node._rpalBondLine=bondLine
        local loc=Text(node,step.location~="" and step.location or "No location",7.3,step.location~="" and C.gold or C.muted)
        loc:SetPoint("TOPLEFT",12,-78);loc:SetWidth(150);loc:SetWordWrap(false)
        local bondCount=math.max(#(step.bondKeys or {}),#(step.bondNames or {}),#bondNames)
        local meta=Text(node,tostring(bondCount).." Bond"..(bondCount==1 and "" or "s").."  •  "..tostring(#incoming).." in / "..tostring(#outgoing).." out",6.8,C.muted)
        meta:SetPoint("BOTTOMLEFT",12,8)
        node._rpalTitleText=name;node._rpalLocationText=loc

        if not readOnly then
            local connector=CreateFrame("Button",nil,node,"BackdropTemplate");connector:SetSize(20,26);connector:SetPoint("RIGHT",8,0)
            Backdrop(connector,C.accent,C.gold,12)
            local arrow=Text(connector,"›",15,C.gold,DISPLAY);arrow:SetPoint("CENTER",0,1)
            connector:SetScript("OnClick",function()StartConnect(step.id)end)
            node._rpalConnector=connector
        end

        node:SetScript("OnClick",function()SelectStep(step.id)end)
        node:SetScript("OnEnter",function(self)self:SetBackdropBorderColor(C.gold[1],C.gold[2],C.gold[3],1)end)
        node:SetScript("OnLeave",function(self)
            local c=(shade._rpalConnectSource==step.id and C.blue) or (shade._rpalSelectedStepID==step.id and C.gold) or C.border
            self:SetBackdropBorderColor(c[1],c[2],c[3],c[4] or 1)
        end)
        if not readOnly then node:SetScript("OnDragStart",function(self)
            if not GetCursorPosition then return end
            local x,y=GetCursorPosition()
            self._dragStartX=x;self._dragStartY=y;self._originX=step.x;self._originY=step.y
            self:SetScript("OnUpdate",function(frame)
                local cx,cy=GetCursorPosition()
                local scale=(canvas.GetEffectiveScale and canvas:GetEffectiveScale()) or (((UIParent.GetEffectiveScale and UIParent:GetEffectiveScale()) or 1)*(shade._rpalZoom or 1))
                step.x=math.max(15,math.min(BOARD_W-NODE_W-15,frame._originX+((cx-frame._dragStartX)/scale)))
                step.y=math.max(15,math.min(BOARD_H-NODE_H-15,frame._originY-((cy-frame._dragStartY)/scale)))
                frame:ClearAllPoints();frame:SetPoint("TOPLEFT",step.x,-step.y)
            end)
        end) end
        if not readOnly then node:SetScript("OnDragStop",function(self)
            self:SetScript("OnUpdate",nil)
            RPAL.UpdateStoryArcStep(arc.id,step.id,{x=step.x,y=step.y})
            RefreshLines()
        end) end
    end

    RefreshInspector=function()
        if shade._rpalInspectorBody then shade._rpalInspectorBody:Hide();shade._rpalInspectorBody:SetParent(nil) end
        if shade._rpalLocationSuggestions then shade._rpalLocationSuggestions:Hide();shade._rpalLocationSuggestions:SetParent(nil);shade._rpalLocationSuggestions=nil end
        shade._rpalTitleEdit=nil;shade._rpalLocationEdit=nil;shade._rpalNoteEdit=nil;shade._rpalEditStepID=nil;shade._rpalEditSignature=nil
        local body=CreateFrame("Frame",nil,inspector);body:SetAllPoints();shade._rpalInspectorBody=body
        local step=arc.steps[shade._rpalSelectedStepID or ""]
        if not step then
            local heading=Text(body,"STORY BOARD",12,C.gold,DISPLAY);heading:SetPoint("TOPLEFT",16,-16)
            local stats=RPAL.GetStoryArcStats(arc)
            local count=Text(body,tostring(stats.steps).." events  •  "..tostring(stats.branches).." branches  •  "..tostring(stats.endings).." endings",8,C.muted)
            count:SetPoint("TOPLEFT",16,-43)
            local help=Text(body,"1. Add an event.\n\n2. Drag it into place.\n\n3. Select it to add a location, note and Bond characters.\n\n4. Press Connect, then choose the next event.\n\n5. Watch the path as a presentation.",10,C.pale)
            help:SetPoint("TOPLEFT",16,-82);help:SetWidth(258);help:SetWordWrap(true)
            return
        end

        if readOnly then
            local outgoing=RPAL.GetStoryArcOutgoing(arc,step.id)
            local kind=arc.rootStepID==step.id and "BEGINNING EVENT" or (#outgoing==0 and "ENDING EVENT" or "STORY EVENT")
            local heading=Text(body,kind,8,C.gold);heading:SetPoint("TOPLEFT",16,-16)
            local eventTitle=Text(body,step.title or "Untitled Event",16,C.pale,DISPLAY);eventTitle:SetPoint("TOPLEFT",16,-42);eventTitle:SetWidth(258);eventTitle:SetWordWrap(true)
            local locationLabel=Text(body,"LOCATION",7,C.muted);locationLabel:SetPoint("TOPLEFT",16,-100)
            local location=Text(body,step.location~="" and step.location or "Not recorded",10,step.location~="" and C.gold or C.muted);location:SetPoint("TOPLEFT",16,-118);location:SetWidth(258)
            local noteLabel=Text(body,"WHAT HAPPENED?",7,C.muted);noteLabel:SetPoint("TOPLEFT",16,-158)
            local notePanel=CreateFrame("Frame",nil,body,"BackdropTemplate");notePanel:SetSize(260,170);notePanel:SetPoint("TOPLEFT",16,-176);Backdrop(notePanel,C.panel2,C.border)
            local note=Text(notePanel,(step.note and step.note~="") and step.note or "No note was shared for this event.",9.5,C.pale)
            note:SetPoint("TOPLEFT",10,-10);note:SetWidth(238);note:SetWordWrap(true)
            local bondNames=RPAL.GetStoryArcBondNames and RPAL.GetStoryArcBondNames(step) or {}
            local peopleLabel=Text(body,"CHARACTERS",7,C.muted);peopleLabel:SetPoint("TOPLEFT",16,-366)
            local people=Text(body,#bondNames>0 and table.concat(bondNames,", ") or "No linked characters",9,#bondNames>0 and C.pale or C.muted)
            people:SetPoint("TOPLEFT",16,-384);people:SetWidth(258);people:SetWordWrap(true)
            local nextLabel=Text(body,"NEXT EVENTS",7,C.muted);nextLabel:SetPoint("TOPLEFT",16,-445)
            if #outgoing==0 then
                local ending=Text(body,"This path ends here.",9,C.gold);ending:SetPoint("TOPLEFT",16,-465)
            else
                for index,link in ipairs(outgoing) do
                    if index<=5 then
                        local target=arc.steps[link.to]
                        local row=Text(body,"→  "..tostring(target and target.title or "Unknown"),8.5,C.pale)
                        row:SetPoint("TOPLEFT",16,-463-((index-1)*25));row:SetWidth(250);row:SetWordWrap(false)
                    end
                end
            end
            local watchStep=Button(body,"Watch from Here",128,30,true);watchStep:SetPoint("BOTTOMRIGHT",-16,16)
            watchStep:SetScript("OnClick",function()RPAL.OpenStoryArcWatch(arc,step.id)end)
            shade._rpalReadonlyInspector=true
            return
        end

        local kind=arc.rootStepID==step.id and "BEGINNING EVENT" or "SELECTED EVENT"
        local heading=Text(body,kind,8,C.gold);heading:SetPoint("TOPLEFT",16,-14)
        local titleLabel=Text(body,"TITLE",7,C.muted);titleLabel:SetPoint("TOPLEFT",16,-42)
        local titleEdit=EditBox(body,260,34,false);titleEdit:SetPoint("TOPLEFT",16,-58);titleEdit:SetText(step.title or "")
        local locLabel=Text(body,"LOCATION",7,C.muted);locLabel:SetPoint("TOPLEFT",16,-103)
        local locEdit=EditBox(body,260,34,false);locEdit:SetPoint("TOPLEFT",16,-119);locEdit:SetText(step.location or "")
        local noteLabel=Text(body,"WHAT HAPPENED?",7,C.muted);noteLabel:SetPoint("TOPLEFT",16,-164)
        local noteEdit=EditBox(body,260,112,true);noteEdit:SetPoint("TOPLEFT",16,-180);noteEdit:SetText(step.note or "")
        shade._rpalTitleEdit=titleEdit;shade._rpalLocationEdit=locEdit;shade._rpalNoteEdit=noteEdit;shade._rpalEditStepID=step.id
        shade._rpalEditSignature=table.concat({titleEdit:GetText() or "",locEdit:GetText() or "",noteEdit:GetText() or ""},"\031")

        local suggestionBox=CreateFrame("Frame",nil,UIParent,"BackdropTemplate")
        suggestionBox:SetSize(260,8);suggestionBox:SetPoint("TOPLEFT",locEdit,"BOTTOMLEFT",0,-2)
        if RPAL.ApplyUILayer then RPAL.ApplyUILayer(suggestionBox,"POPOVER",80) else suggestionBox:SetFrameStrata("TOOLTIP");suggestionBox:SetFrameLevel(12080) end
        if suggestionBox.SetToplevel then suggestionBox:SetToplevel(true) end
        Backdrop(suggestionBox,C.bg,C.gold,12);suggestionBox:Hide();shade._rpalLocationSuggestions=suggestionBox
        local function HideSuggestions()
            if suggestionBox and suggestionBox.Hide then suggestionBox:Hide() end
        end
        local function ChooseSuggestion(value)
            if not value or tostring(value)=="" then return false end
            shade._rpalSelectingLocation=true;shade._rpalApplyingLocation=true
            locEdit:SetText(tostring(value or ""))
            HideSuggestions()
            if CommitInspector then CommitInspector("location") end
            if locEdit.ClearFocus then locEdit:ClearFocus() end
            shade._rpalSelectingLocation=false;shade._rpalApplyingLocation=false
            return true
        end
        local function HighlightSuggestion(index)
            local values=suggestionBox._rpalValues or {}
            if #values==0 then suggestionBox._rpalIndex=nil;return end
            index=math.max(1,math.min(#values,tonumber(index) or 1))
            suggestionBox._rpalIndex=index
            for rowIndex,row in ipairs(suggestionBox._rpalRows or {}) do
                local active=rowIndex==index
                local border=active and C.gold or C.border
                row:SetBackdropBorderColor(border[1],border[2],border[3],active and 1 or (border[4] or 1))
                if row._rpalLabel then
                    local color=active and C.gold or C.pale
                    row._rpalLabel:SetTextColor(color[1],color[2],color[3],color[4] or 1)
                end
            end
        end
        local function CommitHighlightedSuggestion()
            local values=suggestionBox._rpalValues or {}
            return ChooseSuggestion(values[suggestionBox._rpalIndex or 1])
        end
        local function RenderSuggestions()
            if shade._rpalSelectingLocation then return end
            for _,row in ipairs(suggestionBox._rpalRows or {}) do row:Hide();row:SetParent(nil) end
            suggestionBox._rpalFirst=nil;suggestionBox._rpalRows={};suggestionBox._rpalValues={}
            local query=tostring(locEdit:GetText() or ""):gsub("^%s+",""):gsub("%s+$","")
            local suggestions=(#query>=2 and RPAL.GetStoryLocationSuggestions) and RPAL.GetStoryLocationSuggestions(query,5) or {}
            if #suggestions==0 then HideSuggestions();return end
            suggestionBox._rpalValues=suggestions;suggestionBox._rpalIndex=1
            suggestionBox:SetHeight(8+(#suggestions*27))
            if RPAL.SetOpenDropdown then RPAL.SetOpenDropdown(suggestionBox) end
            suggestionBox:Show()
            if suggestionBox.Raise then suggestionBox:Raise() end
            for index,value in ipairs(suggestions) do
                local locationName=value
                local row=CreateFrame("Button",nil,suggestionBox,"BackdropTemplate");row:SetSize(248,23);row:SetPoint("TOPLEFT",6,-5-((index-1)*27))
                Backdrop(row,C.panel2,C.border,10)
                local label=Text(row,locationName,8.5,index==1 and C.gold or C.pale);label:SetPoint("LEFT",8,0);label:SetWidth(228);label:SetWordWrap(false);row._rpalLabel=label
                -- MouseDown runs before the EditBox loses focus.  Using only
                -- OnClick let the focus-lost handler destroy this popup first.
                row:SetScript("OnMouseDown",function()ChooseSuggestion(locationName)end)
                row:SetScript("OnClick",function()if suggestionBox:IsShown() then ChooseSuggestion(locationName) end end)
                row:SetScript("OnEnter",function()HighlightSuggestion(index)end)
                suggestionBox._rpalRows[index]=row
                if index==1 then suggestionBox._rpalFirst=locationName end
            end
            HighlightSuggestion(1)
        end
        locEdit:SetScript("OnTextChanged",function(_,userInput)if userInput~=false then RenderSuggestions() end end)
        locEdit:SetScript("OnEditFocusGained",RenderSuggestions)
        locEdit:SetScript("OnEnterPressed",function(self)
            if not CommitHighlightedSuggestion() then self:ClearFocus() end
        end)
        locEdit:SetScript("OnTabPressed",function()
            if not suggestionBox:IsShown() then RenderSuggestions() end
            local values=suggestionBox._rpalValues or {}
            if #values>0 then HighlightSuggestion(((suggestionBox._rpalIndex or 0)%#values)+1) end
        end)
        locEdit:SetScript("OnEscapePressed",function(self)shade._rpalCancelLocationSuggestion=true;HideSuggestions();self:ClearFocus()end)
        titleEdit:SetScript("OnEnterPressed",function(self)self:ClearFocus()end)
        titleEdit:SetScript("OnEscapePressed",function(self)self:ClearFocus()end)
        local function AutoSaveLostFocus(self)
            if self==locEdit and not shade._rpalApplyingLocation then
                if shade._rpalCancelLocationSuggestion then
                    shade._rpalCancelLocationSuggestion=nil
                elseif suggestionBox:IsShown() and CommitHighlightedSuggestion() then
                    return
                end
            end
            if CommitInspector then CommitInspector("focus") end
            HideSuggestions()
        end
        titleEdit:SetScript("OnEditFocusLost",AutoSaveLostFocus)
        locEdit:SetScript("OnEditFocusLost",AutoSaveLostFocus)
        noteEdit:SetScript("OnEditFocusLost",AutoSaveLostFocus)

        local bondNames=RPAL.GetStoryArcBondNames and RPAL.GetStoryArcBondNames(step) or {}
        local peopleLabel=Text(body,"CHARACTERS FROM BONDS",7,C.muted);peopleLabel:SetPoint("TOPLEFT",16,-306)
        local peopleText=Text(body,#bondNames>0 and table.concat(bondNames,", ") or "No characters linked",8.5,#bondNames>0 and C.pale or C.muted)
        peopleText:SetPoint("TOPLEFT",16,-324);peopleText:SetWidth(168);peopleText:SetWordWrap(false)
        local choose=Button(body,"Choose",76,26,false);choose:SetPoint("TOPRIGHT",-16,-314)
        choose:SetScript("OnClick",function()
            BondPicker(step,function(keys)
                RPAL.UpdateStoryArcStep(arc.id,step.id,{bondKeys=keys})
                RefreshBoard()
            end)
        end)

        local autosaved=Text(body,"AUTO-SAVED WHEN YOU CLICK AWAY",7,C.green);autosaved:SetPoint("TOPLEFT",16,-369)
        local completed=Button(body,step.completed and "✓ Happened" or "Mark Happened",126,30,false);completed:SetPoint("TOPRIGHT",-16,-360)
        completed:SetScript("OnClick",function()
            RPAL.UpdateStoryArcStep(arc.id,step.id,{completed=not step.completed});RefreshBoard()
        end)
        local root=Button(body,arc.rootStepID==step.id and "Starting Event" or "Set as Start",116,28,false);root:SetPoint("TOPLEFT",16,-400)
        root:SetScript("OnClick",function()RPAL.SetStoryArcRoot(arc.id,step.id);RefreshBoard()end)
        local connect=Button(body,"Connect →",116,28,true);connect:SetPoint("TOPRIGHT",-16,-400)
        connect:SetScript("OnClick",function()StartConnect(step.id)end)

        local outLabel=Text(body,"NEXT EVENTS",7,C.muted);outLabel:SetPoint("TOPLEFT",16,-447)
        local outgoing=RPAL.GetStoryArcOutgoing(arc,step.id)
        if #outgoing==0 then
            local ending=Text(body,"No outgoing path — this event is currently an ending.",8,C.muted);ending:SetPoint("TOPLEFT",16,-466);ending:SetWidth(250);ending:SetWordWrap(true)
        else
            for index,link in ipairs(outgoing) do
                if index<=4 then
                    local target=arc.steps[link.to]
                    local row=CreateFrame("Frame",nil,body,"BackdropTemplate");row:SetSize(260,29);row:SetPoint("TOPLEFT",16,-462-((index-1)*34));Backdrop(row,C.panel2,C.border)
                    local name=Text(row,"→  "..tostring(target and target.title or "Unknown"),7.8,C.pale);name:SetPoint("LEFT",8,0);name:SetWidth(208);name:SetWordWrap(false)
                    local remove=Button(row,"×",28,23,false);remove:SetPoint("RIGHT",-3,0)
                    remove:SetScript("OnClick",function()RPAL.DeleteStoryArcLink(arc.id,link.id);RefreshBoard()end)
                end
            end
        end
        local delete=Button(body,"Delete Event",106,28,false);delete:SetPoint("BOTTOMRIGHT",-16,16);delete.label:SetTextColor(C.red[1],C.red[2],C.red[3],1)
        delete:SetScript("OnClick",function()
            Confirm("DELETE STORY EVENT","Delete “"..tostring(step.title).."” and every connection entering or leaving it? This cannot be undone.","Delete Event",function()
                RPAL.DeleteStoryArcStep(arc.id,step.id)
                shade._rpalSelectedStepID=arc.rootStepID or arc.stepOrder[1]
                RefreshBoard()
            end)
        end)
    end

    CommitInspector=function(reason)
        if readOnly or shade._rpalCommitting then return true end
        local stepID=shade._rpalEditStepID
        local titleEdit,locationEdit,noteEdit=shade._rpalTitleEdit,shade._rpalLocationEdit,shade._rpalNoteEdit
        if not stepID or not titleEdit or not locationEdit or not noteEdit or not arc.steps[stepID] then return true end
        local titleValue=tostring(titleEdit:GetText() or "")
        local locationValue=tostring(locationEdit:GetText() or "")
        local noteValue=tostring(noteEdit:GetText() or "")
        local signature=table.concat({titleValue,locationValue,noteValue},"\031")
        if signature==shade._rpalEditSignature then return true end
        shade._rpalCommitting=true
        local ok,err=RPAL.UpdateStoryArcStep(arc.id,stepID,{title=titleValue,location=locationValue,note=noteValue})
        if not ok then
            local previous=arc.steps[stepID]
            titleEdit:SetText(previous and previous.title or "Untitled Event")
            status:SetText(tostring(err or "Event could not be saved."))
            shade._rpalCommitting=false
            return false
        end
        shade._rpalEditSignature=signature
        local node=shade._rpalNodeFrames and shade._rpalNodeFrames[stepID]
        if node and node._rpalTitleText then node._rpalTitleText:SetText(arc.steps[stepID].title or "Untitled Event") end
        if node and node._rpalLocationText then node._rpalLocationText:SetText((arc.steps[stepID].location or "")~="" and arc.steps[stepID].location or "No location") end
        status:SetText("Changes saved automatically"..(reason=="focus" and "." or " before continuing."))
        shade._rpalCommitting=false
        return true
    end
    shade._rpalCommitInspector=CommitInspector

    RefreshBoard=function()
        if CommitInspector and not shade._rpalRefreshing then CommitInspector("refresh") end
        shade._rpalRefreshing=true
        for _,node in pairs(shade._rpalNodeFrames or {}) do if node.Hide then node:Hide();node:SetParent(nil) end end
        shade._rpalNodeFrames={}
        RefreshLines()
        for index,stepID in ipairs(arc.stepOrder or {}) do
            local step=arc.steps[stepID]
            if step then BuildNode(step,index) end
        end
        RefreshInspector()
        local stats=RPAL.GetStoryArcStats(arc)
        shade._rpalStoryStats=stats
        shade._rpalRefreshing=false
    end
    shade._rpalRefreshBoard=RefreshBoard

    add:SetScript("OnClick",function()
        local count=#arc.stepOrder
        local ok,step=RPAL.AddStoryArcStep(arc.id,{
            title="New Event",x=110+((count%10)*270),y=150+(math.floor(count/10)*160),
        })
        if not ok then print("|cffff7777ODYN:|r "..tostring(step));return end
        shade._rpalSelectedStepID=step.id
        status:SetText("New event added. Give it a title, location, note and characters in the inspector.")
        RefreshBoard()
    end)
    cast:SetScript("OnClick",function()
        if CommitInspector then CommitInspector("button") end
        shade._rpalLastBondPicker=BondPicker({bondKeys=arc.bondKeys or {}},function(keys)
            RPAL.UpdateStoryArc(arc.id,{bondKeys=keys})
            cast.label:SetText("Bonds "..tostring(#keys))
            status:SetText(#keys>0 and (tostring(#keys).." file-level Bonds linked. Event-specific Bonds remain separate.") or "File-level Bond list cleared.")
        end,"STORY ARC BONDS","Keep the Story Arc's overall cast here. Event-specific character links remain on each event and appear directly below its title.")
    end)
    share:SetScript("OnClick",function()
        if CommitInspector then CommitInspector("button") end
        if RPAL.OpenStoryArcOnlineExchange then RPAL.OpenStoryArcOnlineExchange(arc);return end
        StoryArcTransfer(arc,function(imported)
            if RPAL.RefreshChronicle then RPAL.RefreshChronicle() end
            RPAL.OpenStoryArcBoard(imported)
        end)
    end)
    export:SetScript("OnClick",function()
        if CommitInspector then CommitInspector("button") end
        StoryArcTransfer(arc,function(imported)
            if RPAL.RefreshChronicle then RPAL.RefreshChronicle() end
            RPAL.OpenStoryArcBoard(imported)
        end)
    end)
    watch:SetScript("OnClick",function()if CommitInspector then CommitInspector("button") end;RPAL.OpenStoryArcWatch(arc,arc.rootStepID)end)
    pin:SetScript("OnClick",function()
        if CommitInspector then CommitInspector("button") end
        local ok,err=RPAL.SetStoryArcPinned(arc.id,not arc.pinned)
        if not ok then print("|cffff7777ODYN:|r "..tostring(err));return end
        pin.label:SetText(arc.pinned and "Unpin" or "Pin")
        status:SetText(arc.pinned and "Pinned to profile. Other ODYN players can inspect this read-only board and Watch it." or "Story Arc removed from your public profile.")
        if RPAL.RefreshChronicle then RPAL.RefreshChronicle() end
    end)
    edit:SetScript("OnClick",function()
        if CommitInspector then CommitInspector("button") end
        StoryArcEditor(arc,function()
            title:SetText(arc.title or "Story Arc")
            if RPAL.RefreshChronicle then RPAL.RefreshChronicle() end
        end)
    end)
    shade:HookScript("OnHide",function(self)
        if CommitInspector then CommitInspector("close") end
        if self._rpalLocationSuggestions then self._rpalLocationSuggestions:Hide();self._rpalLocationSuggestions:SetParent(nil);self._rpalLocationSuggestions=nil end
        if activeBoardShade==self then activeBoardShade=nil end
        if not readOnly and RPAL.RefreshChronicle then RPAL.RefreshChronicle() end
    end)

    ApplyView()
    RefreshBoard()
    return shade,f
end

-- ------------------------------------------------------------
-- Chronicle file shelf
-- ------------------------------------------------------------

function RPAL.RenderStoryArcFiles(parent,onRefresh)
    local C=Palette()
    local arcs=RPAL.GetStoryArcs and RPAL.GetStoryArcs() or {}
    parent._rpalStoryArcFileCount=#arcs
    parent._rpalStoryArcCards={}

    local header=CreateFrame("Frame",nil,parent,"BackdropTemplate");header:SetSize(720,104);header:SetPoint("TOPLEFT",0,0)
    Backdrop(header,C.panel,C.border)
    local glow=header:CreateTexture(nil,"BACKGROUND",nil,-1);glow:SetTexture(SPARK);glow:SetBlendMode("ADD");glow:SetSize(360,120);glow:SetPoint("LEFT",-30,4)
    glow:SetVertexColor(C.gold[1],C.gold[2],C.gold[3],0.13)
    local title=Text(header,"STORY ARC FILES",15,C.gold,DISPLAY);title:SetPoint("TOPLEFT",18,-16)
    local body=Text(header,"Build branching RP histories as visual boards. Events can hold a place, private note and characters from Bonds; Watch turns any connected path into a cinematic recap.",9,C.muted)
    body:SetPoint("TOPLEFT",18,-43);body:SetWidth(390);body:SetWordWrap(true)
    local add=Button(header,"+ New Story Arc",130,34,true);add:SetPoint("RIGHT",-16,0)
    local importArc=Button(header,"Import Arc",92,34,false);importArc:SetPoint("RIGHT",add,"LEFT",-8,0)
    importArc:SetScript("OnClick",function()
        StoryArcTransfer(nil,function(arc)
            if onRefresh then onRefresh() elseif RPAL.RefreshChronicle then RPAL.RefreshChronicle() end
            RPAL.OpenStoryArcBoard(arc)
        end)
    end)
    add:SetScript("OnClick",function()
        ArcFileCreator(function(arc)
            if onRefresh then onRefresh() elseif RPAL.RefreshChronicle then RPAL.RefreshChronicle() end
            RPAL.OpenStoryArcBoard(arc)
        end)
    end)
    parent._rpalNewStoryArcButton=add;parent._rpalImportStoryArcButton=importArc

    if #arcs==0 then
        local empty=CreateFrame("Frame",nil,parent,"BackdropTemplate");empty:SetSize(720,170);empty:SetPoint("TOPLEFT",0,-116);Backdrop(empty,C.panel,C.border)
        local et=Text(empty,"No Story Arc files yet.",14,C.pale,DISPLAY);et:SetPoint("TOP",0,-42)
        local eb=Text(empty,"Create one file for each plot, relationship arc, mystery or campaign. They remain completely separate.",10,C.muted)
        eb:SetPoint("TOP",0,-73);eb:SetWidth(580);eb:SetJustifyH("CENTER");eb:SetWordWrap(true)
        local first=Button(empty,"Create the First Arc",150,34,true);first:SetPoint("BOTTOM",0,26)
        first:SetScript("OnClick",function()
            ArcFileCreator(function(arc)
                if onRefresh then onRefresh() elseif RPAL.RefreshChronicle then RPAL.RefreshChronicle() end
                RPAL.OpenStoryArcBoard(arc)
            end)
        end)
        return 300
    end

    local startY=116
    for index,arc in ipairs(arcs) do
        local currentArc=arc
        local col=(index-1)%2
        local row=math.floor((index-1)/2)
        local card=CreateFrame("Frame",nil,parent,"BackdropTemplate");card:SetSize(352,156);card:SetPoint("TOPLEFT",col*368,-startY-(row*168))
        parent._rpalStoryArcCards[#parent._rpalStoryArcCards+1]=card
        Backdrop(card,C.panel,C.border,16)
        local tab=card:CreateTexture(nil,"BACKGROUND",nil,-1);tab:SetTexture(WHITE);tab:SetSize(92,5);tab:SetPoint("TOPLEFT",10,-2)
        tab:SetVertexColor((arc.completed and C.green or C.gold)[1],(arc.completed and C.green or C.gold)[2],(arc.completed and C.green or C.gold)[3],0.90)
        local fileBadge=arc.pinned and "PINNED STORY" or (arc.completed and "COMPLETED FILE" or "ACTIVE FILE")
        local badge=Text(card,fileBadge,7,arc.pinned and C.blue or (arc.completed and C.green or C.gold));badge:SetPoint("TOPLEFT",14,-14)
        local name=Text(card,arc.title,13,C.pale,DISPLAY);name:SetPoint("TOP",0,-33);name:SetWidth(318);name:SetJustifyH("CENTER");name:SetWordWrap(false)
        card._rpalTitleText=name
        local note=Text(card,(arc.note and arc.note~="") and arc.note or "No file note yet.",8.2,C.muted);note:SetPoint("TOP",0,-57);note:SetWidth(318);note:SetHeight(30);note:SetJustifyH("CENTER");note:SetWordWrap(true)
        local stats=RPAL.GetStoryArcStats(arc)
        local meta=Text(card,tostring(stats.steps).." events  •  "..tostring(stats.branches).." branches  •  "..tostring(stats.endings).." endings",7.5,C.gold)
        meta:SetPoint("TOPLEFT",14,-92)
        local changed=Text(card,"Edited "..DateText(arc.updatedAt or arc.createdAt),6.8,C.muted);changed:SetPoint("TOPLEFT",14,-108)
        local open=Button(card,"Open",58,28,true);open:SetPoint("BOTTOMLEFT",10,10);open:SetScript("OnClick",function()RPAL.OpenStoryArcBoard(currentArc)end)
        local watch=Button(card,"▶ Watch",62,28,false);watch:SetPoint("LEFT",open,"RIGHT",4,0);watch:SetScript("OnClick",function()RPAL.OpenStoryArcWatch(currentArc)end)
        local pin=Button(card,arc.pinned and "Unpin" or "Pin",45,28,false);pin:SetPoint("LEFT",watch,"RIGHT",4,0)
        pin:SetScript("OnClick",function()
            local ok,err=RPAL.SetStoryArcPinned(currentArc.id,not currentArc.pinned)
            if not ok then print("|cffff7777ODYN:|r "..tostring(err));return end
            if onRefresh then onRefresh() elseif RPAL.RefreshChronicle then RPAL.RefreshChronicle() end
        end)
        local share=Button(card,"Share",50,28,false);share:SetPoint("LEFT",pin,"RIGHT",4,0)
        share:SetScript("OnClick",function()
            if RPAL.OpenStoryArcOnlineExchange then RPAL.OpenStoryArcOnlineExchange(currentArc)
            else StoryArcTransfer(currentArc,function(imported)if onRefresh then onRefresh() elseif RPAL.RefreshChronicle then RPAL.RefreshChronicle() end;RPAL.OpenStoryArcBoard(imported)end) end
        end)
        local edit=Button(card,"Edit",44,28,false);edit:SetPoint("LEFT",share,"RIGHT",4,0);edit:SetScript("OnClick",function()StoryArcEditor(currentArc,function()if onRefresh then onRefresh() else RPAL.RefreshChronicle() end end)end)
        local del=Button(card,"×",30,28,false);del:SetPoint("LEFT",edit,"RIGHT",4,0);del.label:SetTextColor(C.red[1],C.red[2],C.red[3],1)
        del:SetScript("OnClick",function()
            Confirm("DELETE STORY ARC FILE","Delete “"..tostring(currentArc.title).."” and every event, branch and note inside it? This cannot be undone.","Delete File",function()
                RPAL.DeleteStoryArc(currentArc.id)
                if onRefresh then onRefresh() elseif RPAL.RefreshChronicle then RPAL.RefreshChronicle() end
            end)
        end)
        card._rpalOpenBoardButton=open;card._rpalWatchButton=watch;card._rpalPinButton=pin;card._rpalShareButton=share
    end
    return startY+(math.ceil(#arcs/2)*168)+8
end

function RPAL.RenderPinnedStoryDock(parent,entries,accent,titleText,emptyText)
    if not parent then return end
    local C=Palette()
    accent=accent or C.gold
    titleText=titleText or "PINNED STORIES"
    emptyText=emptyText or "No Story Arcs pinned."
    entries=entries or {}

    local list={}
    for index=1,3 do
        if entries[index] then list[#list+1]=entries[index] end
    end
    if #list==0 then
        local sparse={}
        for key,item in pairs(entries) do
            local index=tonumber(key)
            if index and item then sparse[#sparse+1]={index=index,item=item} end
        end
        table.sort(sparse,function(a,b)return a.index<b.index end)
        for index=1,math.min(3,#sparse) do list[#list+1]=sparse[index].item end
    end

    local signature={titleText,emptyText,tostring(#list)}
    for _,arc in ipairs(list) do
        signature[#signature+1]=table.concat({
            tostring(arc.title or ""),tostring(arc.note or ""),tostring(arc.rootStepID or ""),
            tostring(#(arc.stepOrder or {})),tostring(#(arc.links or {})),arc.truncated and "1" or "0",
        },"\031")
    end
    local renderKey=table.concat(signature,"\030")
    if parent._rpalPinnedStoryRenderKey==renderKey then return end
    parent._rpalPinnedStoryRenderKey=renderKey

    if parent._rpalStoryChildren then
        for _,obj in ipairs(parent._rpalStoryChildren) do if obj and obj.Hide then obj:Hide();if obj.SetParent then obj:SetParent(nil) end end end
    end
    parent._rpalStoryChildren={}
    local function Keep(obj)parent._rpalStoryChildren[#parent._rpalStoryChildren+1]=obj;return obj end

    Backdrop(parent,C.panel2,accent,14)
    local heading=Keep(Text(parent,titleText,9.5,accent,DISPLAY));heading:SetPoint("TOPLEFT",10,-8)
    local sub=Keep(Text(parent,"READ-ONLY STORY BOARDS",6.5,C.muted));sub:SetPoint("TOPRIGHT",-10,-10)
    local rule=Keep(parent:CreateTexture(nil,"ARTWORK"));rule:SetTexture(WHITE);rule:SetHeight(1)
    rule:SetPoint("TOPLEFT",10,-26);rule:SetPoint("TOPRIGHT",-10,-26);rule:SetVertexColor(accent[1],accent[2],accent[3],0.28)

    if #list==0 then
        local empty=Keep(Text(parent,emptyText,8.2,C.muted));empty:SetPoint("TOPLEFT",10,-41);empty:SetPoint("RIGHT",-10,0);empty:SetWordWrap(false)
        parent._rpalStoryCardCount=0
        return
    end

    local width=(parent.GetWidth and parent:GetWidth()) or 380
    local spacing=6
    local cardW=math.max(100,math.floor((width-20-((#list-1)*spacing))/#list))
    local x=10
    parent._rpalStoryCardCount=#list
    parent._rpalStoryCards={}
    for _,arc in ipairs(list) do
        local currentArc=arc
        local card=Keep(CreateFrame("Button",nil,parent,"BackdropTemplate"));card:SetSize(cardW,38);card:SetPoint("TOPLEFT",x,-32)
        Backdrop(card,C.bg,accent,12)
        local marker=Keep(card:CreateTexture(nil,"ARTWORK"));marker:SetTexture(WHITE);marker:SetPoint("TOPLEFT",2,-3);marker:SetPoint("BOTTOMLEFT",2,3);marker:SetWidth(3)
        marker:SetVertexColor(accent[1],accent[2],accent[3],0.92)
        local name=Keep(Text(card,currentArc.title or "A Story Arc",8.5,C.pale,DISPLAY));name:SetPoint("TOPLEFT",10,-5);name:SetPoint("RIGHT",-25,0);name:SetWordWrap(false)
        local stats=RPAL.GetStoryArcStats and RPAL.GetStoryArcStats(currentArc) or {steps=#(currentArc.stepOrder or {}),branches=0}
        local metaText=tostring(stats.steps or 0).." events • "..tostring(stats.branches or 0).." branches"..(currentArc.truncated and " • partial" or "")
        local meta=Keep(Text(card,metaText,6.5,C.muted));meta:SetPoint("TOPLEFT",10,-22);meta:SetPoint("RIGHT",-25,0);meta:SetWordWrap(false)
        local play=Keep(CreateFrame("Button",nil,card));play:SetSize(22,30);play:SetPoint("RIGHT",-2,0)
        local playText=Keep(Text(play,"▶",9,accent));playText:SetPoint("CENTER")
        play:SetScript("OnClick",function()RPAL.OpenStoryArcWatch(currentArc)end)
        card:SetScript("OnClick",function()RPAL.OpenStoryArcBoard(currentArc,{readOnly=true,public=true})end)
        card:SetScript("OnEnter",function(self)self:SetBackdropBorderColor(accent[1],accent[2],accent[3],1)end)
        card:SetScript("OnLeave",function(self)self:SetBackdropBorderColor(accent[1],accent[2],accent[3],0.62)end)
        card._rpalOpenReadonly=true;card._rpalWatchButton=play;card._rpalStoryArc=currentArc
        parent._rpalStoryCards[#parent._rpalStoryCards+1]=card
        x=x+cardW+spacing
    end
end
