local _, NS = ...

local ui = {
    rows = {},
}

local HEADER_LOGO_TEXTURE = "Interface\\AddOns\\EerstKoffieHeadlines\\Media\\EerstKoffieLogo"
local LAUNCHER_LOGO_TEXTURE = "Interface\\AddOns\\EerstKoffieHeadlines\\Media\\EerstKoffieLauncher"
local DISCORD_BUTTON_TEXTURE = "Interface\\AddOns\\EerstKoffieHeadlines\\Media\\DiscordJoinButton"
local DEFAULT_DISCORD_URL = "https://dsc.gg/eerstkoffie"
local CONFIG_FRAME_GM_BASE_HEIGHT = 680
local CONFIG_FRAME_NON_GM_HEIGHT = 620
local CONFIG_FRAME_EXTRA_ROW_HEIGHT = 28
local CONFIG_DELETE_PERMISSIONS_EXTRA_HEIGHT = 92
local CONFIG_COMM_VIEW_PERMISSIONS_EXTRA_HEIGHT = 92
local CONFIG_FRAME_BOTTOM_SAFE_HEIGHT = 96
local MAIN_FRAME_MIN_WIDTH = 1180
local MAIN_FRAME_MIN_HEIGHT = 620
local CONFIG_RANK_CHECK_BASE_OFFSET = -10
local LOGO_NATIVE_SIZE = 768
local LAUNCHER_SCALES = { 0.5, 1.0, 1.5, 2.0 }
local LAUNCHER_SCALE_MIN = 0.50
local LAUNCHER_SCALE_MAX = 2.00
local LAUNCHER_SCALE_STEP = 0.01
local ELV_BG = { 0.06, 0.06, 0.06, 0.97 }
local ELV_PANEL = { 0.1, 0.1, 0.1, 0.95 }
local ELV_BORDER = { 0.22, 0.22, 0.22, 1 }
local ELV_ACCENT = { 0.16, 0.62, 0.86, 1 }
local FONT_GOLD = { 0.94, 0.78, 0.32 }
local FONT_TITLE = { 0.9, 0.9, 0.9 }
local FONT_LABEL = { 0.78, 0.78, 0.78 }
local FONT_META = { 0.62, 0.62, 0.62 }
local LAUNCHER_BADGE_SIZE = 28
local BUILTIN_FONT_PATHS = {
    frizqt = "Fonts\\FRIZQT__.TTF",
    arialn = "Fonts\\ARIALN.TTF",
    morpheus = "Fonts\\MORPHEUS.TTF",
    skurri = "Fonts\\skurri.ttf",
}
local BUILTIN_FONT_TYPE_OPTIONS = {
    { value = "frizqt", label = "Standaard (Frizqt)" },
    { value = "arialn", label = "Smal (Arial Narrow)" },
    { value = "morpheus", label = "Decoratief (Morpheus)" },
    { value = "skurri", label = "Runisch (Skurri)" },
}
local updateLauncherVisualState
local refreshRows
local styleBackdrop
local styleButton

-- Per-post UI state: whether the comment input or comment list is expanded
local commentInputOpen = {}
local commentExpanded = {}
local commentEditorState = {}

local function flattenThreadedComments(comments)
    local byId = {}
    local children = { [""] = {} }

    for _, c in ipairs(comments or {}) do
        local id = tostring(c.id or "")
        if id ~= "" then
            byId[id] = c
        end
    end

    for _, c in ipairs(comments or {}) do
        local parentId = tostring(c.parentId or "")
        if parentId == "" or not byId[parentId] then
            parentId = ""
        end
        children[parentId] = children[parentId] or {}
        table.insert(children[parentId], c)
    end

    for _, list in pairs(children) do
        table.sort(list, function(a, b)
            local at = tonumber(a.timestamp) or 0
            local bt = tonumber(b.timestamp) or 0
            if at == bt then
                return tostring(a.id or "") < tostring(b.id or "")
            end
            return at < bt
        end)
    end

    local out = {}
    local seen = {}
    local function walk(parentId, depth)
        local list = children[parentId]
        if not list then
            return
        end
        for _, c in ipairs(list) do
            local cid = tostring(c.id or "")
            if cid == "" or not seen[cid] then
                if cid ~= "" then
                    seen[cid] = true
                end
                table.insert(out, {
                    id = cid,
                    author = c.author,
                    timestamp = c.timestamp,
                    text = c.text,
                    parentId = c.parentId or "",
                    depth = depth,
                })
                if cid ~= "" then
                    walk(cid, depth + 1)
                end
            end
        end
    end

    walk("", 1)
    return out
end

local function acquireCommentEntry(row, idx)
    if not row.commentPool then row.commentPool = {} end
    local entry = row.commentPool[idx]
    if not entry then
        entry = CreateFrame("Frame", nil, row)
        entry.bar = entry:CreateTexture(nil, "BACKGROUND")
        entry.bar:SetPoint("TOPLEFT", 0, -1)
        entry.bar:SetWidth(2)
        entry.bar:SetTexture("Interface/Buttons/WHITE8x8")
        entry.bar:SetVertexColor(ELV_ACCENT[1], ELV_ACCENT[2], ELV_ACCENT[3], 0.6)
        entry.metaFS = entry:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        entry.metaFS:SetPoint("TOPLEFT", 10, -4)
        entry.metaFS:SetJustifyH("LEFT")
        entry.metaFS:SetTextColor(0.95, 0.78, 0.32)
        entry.textFS = entry:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        entry.textFS:SetPoint("TOPLEFT", entry.metaFS, "BOTTOMLEFT", 0, -2)
        entry.textFS:SetJustifyH("LEFT")
        entry.textFS:SetJustifyV("TOP")
        entry.textFS:SetTextColor(FONT_LABEL[1], FONT_LABEL[2], FONT_LABEL[3])

        entry.replyBtn = CreateFrame("Button", nil, entry, "UIPanelButtonTemplate")
        entry.replyBtn:SetSize(80, 20)
        entry.replyBtn:SetText("Reageren")
        styleButton(entry.replyBtn)

        entry.editBtn = CreateFrame("Button", nil, entry, "UIPanelButtonTemplate")
        entry.editBtn:SetSize(72, 20)
        entry.editBtn:SetText("Wijzigen")
        styleButton(entry.editBtn)

        entry.deleteBtn = CreateFrame("Button", nil, entry, "UIPanelButtonTemplate")
        entry.deleteBtn:SetSize(92, 20)
        entry.deleteBtn:SetText("Verwijderen")
        styleButton(entry.deleteBtn)

        entry.editorBox = CreateFrame("Frame", nil, entry, "BackdropTemplate")
        styleBackdrop(entry.editorBox)
        entry.editorBox:SetBackdropColor(ELV_BG[1], ELV_BG[2], ELV_BG[3], ELV_BG[4])
        entry.editorBox:SetHeight(30)

        entry.editorInput = CreateFrame("EditBox", nil, entry.editorBox, "InputBoxTemplate")
        entry.editorInput:SetPoint("LEFT", entry.editorBox, "LEFT", 6, 0)
        entry.editorInput:SetPoint("RIGHT", entry.editorBox, "RIGHT", -184, 0)
        entry.editorInput:SetPoint("TOP", entry.editorBox, "TOP", 0, -4)
        entry.editorInput:SetHeight(20)
        entry.editorInput:SetAutoFocus(false)
        entry.editorInput:SetMaxLetters(200)
        entry.editorInput:SetTextColor(FONT_LABEL[1], FONT_LABEL[2], FONT_LABEL[3])

        entry.editorSave = CreateFrame("Button", nil, entry.editorBox, "UIPanelButtonTemplate")
        entry.editorSave:SetSize(84, 22)
        entry.editorSave:SetText("Opslaan")
        entry.editorSave:SetPoint("TOPRIGHT", entry.editorBox, "TOPRIGHT", -94, -3)
        styleButton(entry.editorSave)

        entry.editorCancel = CreateFrame("Button", nil, entry.editorBox, "UIPanelButtonTemplate")
        entry.editorCancel:SetSize(84, 22)
        entry.editorCancel:SetText("Annuleren")
        entry.editorCancel:SetPoint("TOPRIGHT", entry.editorBox, "TOPRIGHT", -4, -3)
        styleButton(entry.editorCancel)

        row.commentPool[idx] = entry
    end
    entry:Show()
    return entry
end

local function showDeleteCommentConfirm(postId, commentId, authorName)
    if not postId or postId == "" or not commentId or commentId == "" then
        return
    end

    local popupDialogs = _G and _G["StaticPopupDialogs"]
    local popupShow = _G and _G["StaticPopup_Show"]
    local yesText = (_G and _G["YES"]) or "Ja"
    local cancelText = (_G and _G["CANCEL"]) or "Annuleren"

    if not popupDialogs or type(popupShow) ~= "function" then
        if NS.DeleteComment then
            local ok = NS.DeleteComment(postId, commentId)
            if not ok then
                UIErrorsFrame:AddMessage("Reactie verwijderen mislukt.", 1, 0.2, 0.2)
            end
        end
        return
    end

    if not popupDialogs.EKH_CONFIRM_DELETE_COMMENT then
        popupDialogs.EKH_CONFIRM_DELETE_COMMENT = {
            text = "Reactie van %s verwijderen?\n\nOok alle onderliggende reacties worden verwijderd.",
            button1 = yesText,
            button2 = cancelText,
            OnAccept = function(_, data)
                if not data or not data.postId or not data.commentId then
                    return
                end
                if NS.DeleteComment then
                    local ok = NS.DeleteComment(data.postId, data.commentId)
                    if not ok then
                        UIErrorsFrame:AddMessage("Reactie verwijderen mislukt.", 1, 0.2, 0.2)
                    end
                end
            end,
            timeout = 0,
            whileDead = 1,
            hideOnEscape = 1,
            preferredIndex = 3,
        }
    end

    local authorLabel = tostring(authorName or "deze speler")
    popupShow("EKH_CONFIRM_DELETE_COMMENT", authorLabel, nil, {
        postId = postId,
        commentId = commentId,
    })
end

local function registerEscClosableFrame(frameName)
    if type(frameName) ~= "string" or frameName == "" then
        return
    end

    if type(UISpecialFrames) ~= "table" then
        return
    end

    for i = 1, #UISpecialFrames do
        if UISpecialFrames[i] == frameName then
            return
        end
    end

    table.insert(UISpecialFrames, frameName)
end

local function unregisterEscClosableFrame(frameName)
    if type(frameName) ~= "string" or frameName == "" then
        return
    end

    if type(UISpecialFrames) ~= "table" then
        return
    end

    for i = #UISpecialFrames, 1, -1 do
        if UISpecialFrames[i] == frameName then
            table.remove(UISpecialFrames, i)
        end
    end
end

local function restoreMainEscRegistrationDeferred()
    local function restoreNow()
        if ui.configFrame and ui.configFrame:IsShown() then
            return
        end
        registerEscClosableFrame("EKHMainFrame")
    end

    if C_Timer and C_Timer.After then
        C_Timer.After(0, restoreNow)
        return
    end

    local waiter = CreateFrame("Frame")
    waiter:SetScript("OnUpdate", function(self)
        self:SetScript("OnUpdate", nil)
        restoreNow()
    end)
end

local function normalizeFontType(value)
    local raw = tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "")
    if raw == "" then
        return "frizqt"
    end

    local normalized = raw:lower():gsub("%s+", "")
    if BUILTIN_FONT_PATHS[normalized] then
        return normalized
    end

    if raw:match("^sm:.+") then
        return raw
    end

    return "frizqt"
end

local function getSharedMediaFonts()
    if not LibStub then
        return nil, {}
    end

    local lsm = LibStub("LibSharedMedia-3.0", true)
    if not lsm then
        return nil, {}
    end

    local fontTable = lsm:HashTable("font")
    if not fontTable then
        return lsm, {}
    end

    local options = {}
    for name, path in pairs(fontTable) do
        if type(name) == "string" and type(path) == "string" and path ~= "" then
            table.insert(options, {
                value = "sm:" .. name,
                label = "SharedMedia: " .. name,
            })
        end
    end

    table.sort(options, function(a, b)
        return a.label:lower() < b.label:lower()
    end)

    return lsm, options
end

local function buildFontTypeOptions()
    local options = {}

    for _, option in ipairs(BUILTIN_FONT_TYPE_OPTIONS) do
        table.insert(options, option)
    end

    local _, sharedOptions = getSharedMediaFonts()
    for _, option in ipairs(sharedOptions) do
        table.insert(options, option)
    end

    return options
end

local function resolveFontPath(fontType)
    local normalized = normalizeFontType(fontType)

    if BUILTIN_FONT_PATHS[normalized] then
        return BUILTIN_FONT_PATHS[normalized]
    end

    if normalized:match("^sm:.+") then
        local lsm = LibStub and LibStub("LibSharedMedia-3.0", true)
        if lsm and lsm.Fetch then
            local sharedName = normalized:sub(4)
            local fetched = lsm:Fetch("font", sharedName, true)
            if type(fetched) == "string" and fetched ~= "" then
                return fetched
            end
        end
    end

    return BUILTIN_FONT_PATHS.frizqt or STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF"
end

local function getFontTypeLabel(value)
    local normalized = normalizeFontType(value)
    for _, option in ipairs(buildFontTypeOptions()) do
        if option.value == normalized then
            return option.label
        end
    end
    return "Standaard (Frizqt)"
end

local function getHeadlineFontConfig()
    local cfg = NS.GetConfig and NS.GetConfig() or { fontType = "frizqt", fontSize = 14 }
    local fontType = normalizeFontType(cfg.fontType)
    local fontSize = tonumber(cfg.fontSize) or 14

    local fontPath = resolveFontPath(fontType)
    if not fontPath then
        fontPath = "Fonts\\FRIZQT__.TTF"
    end

    if fontSize < 10 then
        fontSize = 10
    elseif fontSize > 24 then
        fontSize = 24
    end

    return fontPath, math.floor(fontSize)
end

local function getHeaderDiscordUrl()
    local cfg = NS.GetConfig and NS.GetConfig() or { discordHeaderUrl = DEFAULT_DISCORD_URL }
    local url = tostring(cfg.discordHeaderUrl or ""):gsub("^%s+", ""):gsub("%s+$", "")
    if url == "" then
        return DEFAULT_DISCORD_URL
    end
    return url
end

local function setSafeFont(fontString, fontPath, fontSize, flags)
    if not fontString then
        return
    end

    local ok = fontString:SetFont(fontPath, fontSize, flags)
    if (not ok) and STANDARD_TEXT_FONT and STANDARD_TEXT_FONT ~= fontPath then
        fontString:SetFont(STANDARD_TEXT_FONT, fontSize, flags)
    end
end

local function applyRowFonts(row)
    if not row then
        return
    end

    local fontPath, fontSize = getHeadlineFontConfig()
    setSafeFont(row.title, fontPath, math.max(12, fontSize + 2), "")
    setSafeFont(row.meta, fontPath, math.max(10, fontSize - 2), "")
    setSafeFont(row.body, fontPath, fontSize, "")
end

local function attachTabNavigation(editBoxes)
    if not editBoxes or #editBoxes < 2 then
        return
    end

    for i, editBox in ipairs(editBoxes) do
        local index = i
        editBox:SetScript("OnTabPressed", function()
            local direction = IsShiftKeyDown() and -1 or 1
            local nextIndex = index + direction

            if nextIndex < 1 then
                nextIndex = #editBoxes
            elseif nextIndex > #editBoxes then
                nextIndex = 1
            end

            local nextEdit = editBoxes[nextIndex]
            if nextEdit then
                nextEdit:SetFocus()
            end
        end)
    end
end

styleBackdrop = function(frame, edgeFile, edgeSize)
    frame:SetBackdrop({
        bgFile = "Interface/Buttons/WHITE8x8",
        edgeFile = edgeFile or "Interface/Buttons/WHITE8x8",
        edgeSize = edgeSize or 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    frame:SetBackdropBorderColor(ELV_BORDER[1], ELV_BORDER[2], ELV_BORDER[3], ELV_BORDER[4])
end

styleButton = function(button)
    button:SetNormalTexture("Interface/Buttons/WHITE8x8")
    button:SetHighlightTexture("Interface/Buttons/WHITE8x8")
    button:SetPushedTexture("Interface/Buttons/WHITE8x8")
    button:SetDisabledTexture("Interface/Buttons/WHITE8x8")

    local n = button:GetNormalTexture()
    if n then
        n:SetAllPoints()
        n:SetVertexColor(0.14, 0.14, 0.14, 1)
    end

    local h = button:GetHighlightTexture()
    if h then
        h:SetAllPoints()
        h:SetVertexColor(ELV_ACCENT[1], ELV_ACCENT[2], ELV_ACCENT[3], 0.22)
    end

    local p = button:GetPushedTexture()
    if p then
        p:SetAllPoints()
        p:SetVertexColor(0.08, 0.08, 0.08, 1)
    end

    local d = button:GetDisabledTexture()
    if d then
        d:SetAllPoints()
        d:SetVertexColor(0.18, 0.18, 0.18, 0.8)
    end

    button:SetNormalFontObject("GameFontHighlightSmall")

    local text = button:GetFontString()
    if text then
        text:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])
        text:SetShadowColor(0, 0, 0, 0.85)
        text:SetShadowOffset(1, -1)
    end
end

local function styleCloseButton(button)
    button:SetSize(20, 20)
    button:SetNormalTexture("Interface/Buttons/WHITE8x8")
    button:SetHighlightTexture("Interface/Buttons/WHITE8x8")
    button:SetPushedTexture("Interface/Buttons/WHITE8x8")

    local n = button:GetNormalTexture()
    if n then
        n:SetAllPoints()
        n:SetVertexColor(0.14, 0.14, 0.14, 1)
    end

    local h = button:GetHighlightTexture()
    if h then
        h:SetAllPoints()
        h:SetVertexColor(ELV_ACCENT[1], ELV_ACCENT[2], ELV_ACCENT[3], 0.22)
    end

    local p = button:GetPushedTexture()
    if p then
        p:SetAllPoints()
        p:SetVertexColor(0.08, 0.08, 0.08, 1)
    end

    if not button._elvX then
        local x = button:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        x:SetPoint("CENTER", 0, 0)
        x:SetText("x")
        x:SetTextColor(0.9, 0.9, 0.9)
        button._elvX = x
    end
end

local function styleEditBox(editBox)
    local regions = { editBox:GetRegions() }
    for i = 1, #regions do
        local region = regions[i]
        if region and region:GetObjectType() == "Texture" then
            region:SetAlpha(0)
        end
    end

    if not editBox._elvBg then
        local bg = CreateFrame("Frame", nil, editBox, "BackdropTemplate")
        bg:SetPoint("TOPLEFT", -2, 2)
        bg:SetPoint("BOTTOMRIGHT", 2, -2)
        bg:SetFrameLevel(math.max(0, editBox:GetFrameLevel() - 1))
        styleBackdrop(bg)
        bg:SetBackdropColor(0.08, 0.08, 0.08, 0.96)
        editBox._elvBg = bg
    end

    editBox:SetTextInsets(6, 6, 2, 2)
end

local function styleCheckButton(checkButton)
    local normal = checkButton:GetNormalTexture()
    if normal then
        normal:SetAlpha(0)
    end

    local pushed = checkButton:GetPushedTexture()
    if pushed then
        pushed:SetAlpha(0)
    end

    local highlight = checkButton:GetHighlightTexture()
    if highlight then
        highlight:SetAlpha(0)
    end

    if not checkButton._elvBox then
        local box = CreateFrame("Frame", nil, checkButton, "BackdropTemplate")
        box:SetSize(16, 16)
        box:SetPoint("LEFT", 3, 0)
        styleBackdrop(box)
        box:SetBackdropColor(0.08, 0.08, 0.08, 0.96)

        local tick = box:CreateTexture(nil, "OVERLAY")
        tick:SetPoint("TOPLEFT", 3, -3)
        tick:SetPoint("BOTTOMRIGHT", -3, 3)
        tick:SetTexture("Interface/Buttons/WHITE8x8")
        tick:SetVertexColor(ELV_ACCENT[1], ELV_ACCENT[2], ELV_ACCENT[3], 0.9)
        tick:Hide()

        checkButton._elvBox = box
        checkButton._elvTick = tick
    end

    local function updateTick()
        if checkButton:GetChecked() then
            checkButton._elvTick:Show()
        else
            checkButton._elvTick:Hide()
        end
    end

    checkButton:HookScript("OnClick", updateTick)
    checkButton:HookScript("OnShow", updateTick)
    updateTick()

    if checkButton.text then
        checkButton.text:SetTextColor(0.82, 0.82, 0.82)
    end
end

local function styleScrollBar(scrollFrame)
    if not scrollFrame then
        return
    end

    local sb = scrollFrame.ScrollBar or scrollFrame.scrollBar
    if not sb then
        return
    end

    local thumb = sb.ThumbTexture or sb.thumbTexture or sb:GetThumbTexture()
    if thumb then
        thumb:SetTexture("Interface/Buttons/WHITE8x8")
        thumb:SetVertexColor(ELV_ACCENT[1], ELV_ACCENT[2], ELV_ACCENT[3], 0.9)
    end

    local bg = sb.TrackBG or sb._elvTrack
    if not bg then
        bg = sb:CreateTexture(nil, "BACKGROUND")
        bg:SetAllPoints()
        sb._elvTrack = bg
    end
    bg:SetTexture("Interface/Buttons/WHITE8x8")
    bg:SetVertexColor(0.08, 0.08, 0.08, 0.95)

    local up = sb.ScrollUpButton
    if up then
        up:SetNormalTexture("Interface/Buttons/WHITE8x8")
        up:SetHighlightTexture("Interface/Buttons/WHITE8x8")
        up:SetPushedTexture("Interface/Buttons/WHITE8x8")
        local t = up:GetNormalTexture()
        if t then
            t:SetAllPoints()
            t:SetVertexColor(0.14, 0.14, 0.14, 1)
        end
    end

    local down = sb.ScrollDownButton
    if down then
        down:SetNormalTexture("Interface/Buttons/WHITE8x8")
        down:SetHighlightTexture("Interface/Buttons/WHITE8x8")
        down:SetPushedTexture("Interface/Buttons/WHITE8x8")
        local t = down:GetNormalTexture()
        if t then
            t:SetAllPoints()
            t:SetVertexColor(0.14, 0.14, 0.14, 1)
        end
    end
end

local function addAbstractLayers(parent)
    local base = parent:CreateTexture(nil, "BACKGROUND", nil, -8)
    base:SetAllPoints()
    base:SetTexture("Interface/Buttons/WHITE8x8")
    base:SetVertexColor(0.06, 0.06, 0.06, 0.98)
end

local function triggerLogoPulse()
    if not ui.logoPulseAnim then
        return
    end

    ui.logoPulseAnim:Stop()
    ui.logoPulseAnim:Play()
end

local function isMainShown()
    return ui.frame and ui.frame:IsShown()
end

local function launcherFeedback(text)
    UIErrorsFrame:AddMessage(text, 0.95, 0.8, 0.45)
end

local function findClosestLauncherScaleIndex(scaleValue)
    local numericValue = tonumber(scaleValue) or LAUNCHER_SCALES[2]
    local chosenIndex = 2
    local bestDiff = math.huge

    for i, val in ipairs(LAUNCHER_SCALES) do
        local diff = math.abs(numericValue - val)
        if diff < bestDiff then
            bestDiff = diff
            chosenIndex = i
        end
    end

    return chosenIndex
end

local function getLauncherScaleLabel(scaleValue)
    local pct = math.floor(((tonumber(scaleValue) or LAUNCHER_SCALES[2]) * 100) + 0.5)
    return string.format("%d%%", pct)
end

local function normalizeLauncherScaleValue(scaleValue)
    local numeric = tonumber(scaleValue) or LAUNCHER_SCALES[2]
    if numeric < LAUNCHER_SCALE_MIN then
        numeric = LAUNCHER_SCALE_MIN
    elseif numeric > LAUNCHER_SCALE_MAX then
        numeric = LAUNCHER_SCALE_MAX
    end

    numeric = math.floor((numeric / LAUNCHER_SCALE_STEP) + 0.5) * LAUNCHER_SCALE_STEP
    return math.floor((numeric * 100) + 0.5) / 100
end

local function setLauncherScale(scaleValue, persistToConfig, showToast)
    local chosenScale = normalizeLauncherScaleValue(scaleValue)
    local chosenIndex = findClosestLauncherScaleIndex(chosenScale)

    ui.launcherScaleIndex = chosenIndex

    if ui.launcherButton then
        ui.launcherButton:SetScale(chosenScale)
    end

    if persistToConfig and NS.UpdateConfig then
        NS.UpdateConfig({ launcherScale = chosenScale })
    end

    if showToast then
        launcherFeedback("Launcher schaal: " .. getLauncherScaleLabel(chosenScale))
    end

    return chosenScale
end

local function cycleLauncherScale()
    local current = ui.launcherScaleIndex or 2
    local nextIndex = current + 1
    if nextIndex > #LAUNCHER_SCALES then
        nextIndex = 1
    end

    setLauncherScale(LAUNCHER_SCALES[nextIndex], true, true)
end

local function temporarilyHideLauncher(seconds)
    seconds = seconds or 15
    ui.launcherHiddenUntil = GetTime() + seconds
    updateLauncherVisualState()
    launcherFeedback("Launcher tijdelijk verborgen (" .. tostring(seconds) .. "s). Gebruik /ekh om de launcher weer te geven.")
end

local function createLauncherButton()
    if ui.launcherButton then
        return ui.launcherButton
    end

    local button = CreateFrame("Button", "EKHLauncherButton", UIParent, "BackdropTemplate")
    button:SetSize(100, 100)
    local cfg = NS.GetConfig and NS.GetConfig() or {
        launcherPoint = "TOPLEFT",
        launcherRelativePoint = "TOPLEFT",
        launcherX = 10,
        launcherY = -180,
        launcherScale = 0.2,
    }

    local initialScale = normalizeLauncherScaleValue(cfg.launcherScale or 0.2)
    local chosenIndex = findClosestLauncherScaleIndex(initialScale)
    ui.launcherScaleIndex = chosenIndex
    button:SetScale(initialScale)
    button:SetPoint(
        cfg.launcherPoint or "TOPLEFT",
        UIParent,
        cfg.launcherRelativePoint or cfg.launcherPoint or "TOPLEFT",
        cfg.launcherX or 10,
        cfg.launcherY or -180
    )
    button:SetFrameStrata("MEDIUM")
    button:SetMovable(true)
    button:EnableMouse(true)
    styleBackdrop(button)
    button:SetBackdropColor(0, 0, 0, 0)
    button:SetBackdropBorderColor(0, 0, 0, 0)
    button:RegisterForDrag("LeftButton")
    button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    button:SetScript("OnDragStart", button.StartMoving)
    button:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()

        local point, _, relativePoint, xOfs, yOfs = self:GetPoint(1)
        point = point or "TOPLEFT"
        relativePoint = relativePoint or point
        xOfs = xOfs or 10
        yOfs = yOfs or -180

        -- Keep launcher anchored against UIParent for stable persistence across reloads.
        self:ClearAllPoints()
        self:SetPoint(point, UIParent, relativePoint, xOfs, yOfs)

        if NS.UpdateConfig then
            NS.UpdateConfig({
                launcherPoint = point,
                launcherRelativePoint = relativePoint,
                launcherX = xOfs,
                launcherY = yOfs,
                launcherScale = self:GetScale() or 0.2,
            })
        end
    end)

    local logo = button:CreateTexture(nil, "ARTWORK")
    logo:SetPoint("TOPLEFT", 6, -6)
    logo:SetPoint("BOTTOMRIGHT", -6, 6)
    logo:SetTexture(LAUNCHER_LOGO_TEXTURE)

    local fallback = button:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    fallback:SetPoint("CENTER")
    fallback:SetText("EKH")
    fallback:SetTextColor(0.93, 0.82, 0.58)

    if logo:GetTexture() then
        fallback:Hide()
    else
        fallback:Show()
    end

    local pulse = button:CreateTexture(nil, "OVERLAY")
    pulse:SetPoint("TOPLEFT", -20, 20)
    pulse:SetPoint("BOTTOMRIGHT", 20, -20)
    pulse:SetTexture("Interface/Buttons/WHITE8x8")
    pulse:SetVertexColor(0.96, 0.7, 0.28, 0)

    local pulseAnim = pulse:CreateAnimationGroup()
    pulseAnim:SetLooping("REPEAT")
    local inA = pulseAnim:CreateAnimation("Alpha")
    inA:SetOrder(1)
    inA:SetFromAlpha(0)
    inA:SetToAlpha(0.42)
    inA:SetDuration(0.18)

    local outA = pulseAnim:CreateAnimation("Alpha")
    outA:SetOrder(2)
    outA:SetFromAlpha(0.42)
    outA:SetToAlpha(0)
    outA:SetDuration(0.58)

    local badge = CreateFrame("Frame", nil, button)
    badge:SetSize(LAUNCHER_BADGE_SIZE, LAUNCHER_BADGE_SIZE)
    badge:SetPoint("BOTTOMRIGHT", 5, -5)
    badge:Hide()

    local badgeBackground = badge:CreateTexture(nil, "BACKGROUND")
    badgeBackground:SetAllPoints()
    badgeBackground:SetTexture("Interface/COMMON/Indicator-Red")
    badgeBackground:SetVertexColor(0.92, 0.12, 0.12, 1)

    local badgeText = badge:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    badgeText:SetPoint("CENTER")
    badgeText:SetTextColor(1, 1, 1)
    badgeText:SetShadowColor(0, 0, 0, 0.85)
    badgeText:SetShadowOffset(1, -1)

    button:SetScript("OnClick", function(_, mouseButton)
        if IsShiftKeyDown() then
            if mouseButton == "RightButton" then
                temporarilyHideLauncher(15)
            else
                cycleLauncherScale()
            end
            return
        end

        if mouseButton == "RightButton" then
            if NS.ToggleConfigFrame and (not ui.configFrame or not ui.configFrame:IsShown()) then
                NS.ToggleConfigFrame()
            end
            return
        end

        if NS.ToggleMainFrame then
            NS.ToggleMainFrame()
        end
    end)

    button:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:ClearLines()
        GameTooltip:AddLine("Eerst Koffie Headlines", FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])
        GameTooltip:AddLine("Linkermuisknop: Open headlines", FONT_LABEL[1], FONT_LABEL[2], FONT_LABEL[3])
        GameTooltip:AddLine("Rechtermuisknop: Open instellingen", FONT_LABEL[1], FONT_LABEL[2], FONT_LABEL[3])
        GameTooltip:AddLine("Shift + Linkermuisknop: Pas launchergrootte aan", FONT_LABEL[1], FONT_LABEL[2], FONT_LABEL[3])
        GameTooltip:AddLine("Shift + Rechtermuisknop: Sluit launcher tijdelijk", FONT_LABEL[1], FONT_LABEL[2], FONT_LABEL[3])
        GameTooltip:Show()
    end)

    button:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    ui.launcherButton = button
    ui.launcherBadge = badge
    ui.launcherBadgeText = badgeText
    ui.launcherPulseAnim = pulseAnim

    return button
end

updateLauncherVisualState = function()
    createLauncherButton()

    if ui.launcherHiddenUntil and GetTime() < ui.launcherHiddenUntil then
        ui.launcherButton:Hide()
        if ui.launcherPulseAnim:IsPlaying() then
            ui.launcherPulseAnim:Stop()
        end
        ui.launcherBadge:Hide()
        return
    end

    if ui.launcherHiddenUntil and GetTime() >= ui.launcherHiddenUntil then
        ui.launcherHiddenUntil = nil
    end

    if isMainShown() then
        ui.launcherButton:Hide()
        if ui.launcherPulseAnim:IsPlaying() then
            ui.launcherPulseAnim:Stop()
        end
        ui.launcherBadge:Hide()
        return
    end

    ui.launcherButton:Show()

    local unseen = ui.unseenCount or 0
    if unseen > 0 then
        local label = unseen > 99 and "99+" or tostring(unseen)
        ui.launcherBadgeText:SetText(label)
        ui.launcherBadge:Show()
    else
        ui.launcherBadge:Hide()
        if ui.launcherPulseAnim:IsPlaying() then
            ui.launcherPulseAnim:Stop()
        end
    end
end

local function extractLinks(text)
    local links = {}
    for url in string.gmatch(text or "", "https?://[%w%-%._~:/%?#%[%]@!$&'()*+,;=%%]+") do
        table.insert(links, url)
    end
    return links
end

local function getDisplayPosts(posts)
    if ui.sortNewestFirst ~= false then
        return posts
    end

    local reversed = {}
    for i = #posts, 1, -1 do
        table.insert(reversed, posts[i])
    end
    return reversed
end

local function normalizeSearchQuery(value)
    local q = tostring(value or "")
    q = q:gsub("^%s+", ""):gsub("%s+$", "")
    q = q:gsub("%s+", " ")
    return q:lower()
end

local function postMatchesSearch(post, normalizedQuery)
    if normalizedQuery == "" then
        return true
    end

    local haystack = table.concat({
        tostring(post and post.title or ""),
        tostring(post and post.text or ""),
        tostring(post and post.author or ""),
        tostring(post and post.discordLink or ""),
    }, " "):lower()

    return string.find(haystack, normalizedQuery, 1, true) ~= nil
end

local function showCopyDialog(value)
    if not ui.copyFrame then
        local frame = CreateFrame("Frame", "EKHCopyFrame", UIParent, "BackdropTemplate")
        frame:SetSize(560, 220)
        frame:SetPoint("CENTER")
        frame:SetFrameStrata("DIALOG")
        styleBackdrop(frame)
        frame:SetBackdropColor(ELV_BG[1], ELV_BG[2], ELV_BG[3], ELV_BG[4])

        local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        title:SetPoint("TOP", 0, -12)
        title:SetText("Kopieer links/tekst")
        title:SetTextColor(FONT_TITLE[1], FONT_TITLE[2], FONT_TITLE[3])

        local scroll = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate")
        scroll:SetPoint("TOPLEFT", 18, -38)
        scroll:SetPoint("BOTTOMRIGHT", -36, 44)
        styleScrollBar(scroll)

        local box = CreateFrame("EditBox", nil, scroll)
        box:SetMultiLine(true)
        box:SetAutoFocus(true)
        box:SetFontObject("ChatFontNormal")
        box:SetWidth(490)
        box:SetHeight(150)
        box:SetScript("OnEscapePressed", function()
            frame:Hide()
        end)
        scroll:SetScrollChild(box)
        frame.box = box

        local hint = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        hint:SetPoint("BOTTOMLEFT", 18, 20)
        hint:SetText("Ctrl+C om te kopieren")
        hint:SetTextColor(FONT_LABEL[1], FONT_LABEL[2], FONT_LABEL[3])

        local close = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
        close:SetSize(100, 24)
        close:SetPoint("BOTTOMRIGHT", -16, 14)
        close:SetText("Sluiten")
        styleButton(close)
        close:SetScript("OnClick", function()
            frame:Hide()
        end)

        ui.copyFrame = frame
    end

    ui.copyFrame:Show()
    ui.copyFrame.box:SetText(value or "")
    ui.copyFrame.box:HighlightText()
    ui.copyFrame.box:SetFocus()
end

local function refreshCommunicationStatusTexts()
    local stats = NS.GetCommunicationStats and NS.GetCommunicationStats() or {
        onlineGuildMembers = 0,
        communicatingClients = 0,
    }

    local text = string.format(
        "Guild online: %d | Communicerende clients: %d",
        tonumber(stats.onlineGuildMembers) or 0,
        tonumber(stats.communicatingClients) or 0
    )

    if ui.configCommStatus then
        ui.configCommStatus:SetText(text)
    end

    if ui.mainCommStatus then
        ui.mainCommStatus:SetText(text)
    end
end

local function truncateNameToFit(coloredName, maxWidthPixels)
    if not coloredName then
        return coloredName
    end

    -- Create a temporary FontString for measuring
    local measurer = ui.mainMembersContent:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    measurer:SetText(coloredName)
    
    local currentWidth = measurer:GetStringWidth()
    if currentWidth <= maxWidthPixels then
        measurer:Hide()
        return coloredName
    end

    -- Need to truncate - extract base name without color codes
    local strippedName = coloredName:gsub("|c[fF][fF]%x%x%x%x%x%x", ""):gsub("|r", "")
    
    if strippedName == "" then
        measurer:Hide()
        return coloredName
    end

    -- Measure ellipsis
    measurer:SetText("...")
    local ellipsisWidth = measurer:GetStringWidth()
    local availableWidth = maxWidthPixels - ellipsisWidth

    -- Binary search / iterative truncation to fit within available width
    local truncatedName = strippedName
    for i = #strippedName, 1, -1 do
        truncatedName = strippedName:sub(1, i)
        measurer:SetText(truncatedName)
        if measurer:GetStringWidth() <= availableWidth then
            break
        end
    end

    -- Add color codes back with ellipsis
    local result = truncatedName .. "..."
    
    -- Check if original had color codes and restore them
    if coloredName:match("|c[fF][fF]%x%x%x%x%x%x") then
        local colorCode = coloredName:match("|c[fF][fF]%x%x%x%x%x%x")
        result = colorCode .. result .. "|r"
    end

    measurer:Hide()
    return result
end

local function refreshCommunicationMembersPanel()
    if not ui.mainMembersPanel then
        return
    end

    local canViewMembers = NS.CanViewCommunicationMembers and NS.CanViewCommunicationMembers() or false
    if not canViewMembers then
        ui.mainMembersPanel:Hide()
        return
    end

    ui.mainMembersPanel:Show()

    local members = NS.GetAllOnlineGuildMembers and NS.GetAllOnlineGuildMembers() or {}
    local rows = ui.mainMembersRows or {}

    for i = 1, #members do
        local row = rows[i]
        if not row then
            row = ui.mainMembersContent:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            row:SetPoint("TOPLEFT", 12, -((i - 1) * 18))
            row:SetPoint("TOPRIGHT", -4, -((i - 1) * 18))
            row:SetJustifyH("LEFT")
            rows[i] = row
        end

        local memberData = members[i]
        local rawName = memberData.name or ""
        local isCommunicating = memberData.isCommunicating or false
        
        -- Get class color for name
        local classHex = NS.GetClassColoredMemberName and NS.GetClassColoredMemberName(rawName) or rawName
        -- Extract just the hex code
        local hex = classHex:match("|cff(%x%x%x%x%x%x)")
        if not hex then
            hex = "c6b5a5" -- default text color
        end
        
        -- Build display text (name only)
        local truncatedName = truncateNameToFit(rawName, 230)
        
        -- Apply color: communicating members in class color, non-communicating in gray
        if isCommunicating then
            row:SetText("|cff" .. hex .. truncatedName .. "|r")
            row:SetTextColor(1, 1, 1) -- Will be overridden by color code
        else
            row:SetText(truncatedName)
            row:SetTextColor(0.5, 0.5, 0.5) -- Gray for non-communicating
        end
        
        row:Show()
    end

    for i = #members + 1, #rows do
        rows[i]:Hide()
    end

    ui.mainMembersRows = rows

    if #members == 0 then
        ui.mainMembersEmptyText:Show()
        ui.mainMembersEmptyText:SetText("Geen actieve leden")
        ui.mainMembersContent:SetHeight(20)
    else
        ui.mainMembersEmptyText:Hide()
        ui.mainMembersContent:SetHeight(math.max(20, #members * 18 + 4))
    end
end

local function createConfigFrame()
    if ui.configFrame then
        return ui.configFrame
    end

    local frame = CreateFrame("Frame", "EKHConfigFrame", UIParent, "BackdropTemplate")
    registerEscClosableFrame("EKHConfigFrame")
    frame:SetScript("OnShow", function()
        unregisterEscClosableFrame("EKHMainFrame")
        registerEscClosableFrame("EKHConfigFrame")
    end)
    frame:SetScript("OnHide", function()
        restoreMainEscRegistrationDeferred()
        registerEscClosableFrame("EKHConfigFrame")

        local cfg = NS.GetConfig and NS.GetConfig() or { launcherScale = LAUNCHER_SCALES[2] }
        setLauncherScale(cfg.launcherScale or LAUNCHER_SCALES[2], false, false)
    end)
    frame:SetSize(460, CONFIG_FRAME_GM_BASE_HEIGHT)
    frame:SetPoint("CENTER", 0, 20)
    frame:SetFrameStrata("DIALOG")
    frame:SetMovable(true)
    frame:EnableMouse(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetScript("OnDragStart", frame.StartMoving)
    frame:SetScript("OnDragStop", frame.StopMovingOrSizing)
    styleBackdrop(frame)
    frame:SetBackdropColor(ELV_BG[1], ELV_BG[2], ELV_BG[3], ELV_BG[4])
    frame:Hide()

    local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 16, -14)
    title:SetText("Headlines instellingen")
    title:SetTextColor(FONT_TITLE[1], FONT_TITLE[2], FONT_TITLE[3])

    local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", -6, -6)
    styleCloseButton(close)

    local generalSection = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    generalSection:SetPoint("TOPLEFT", 18, -40)
    generalSection:SetText("Algemeen")
    generalSection:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])

    local typographySection = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    typographySection:SetPoint("TOPLEFT", 248, -40)
    typographySection:SetText("Typografie")
    typographySection:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])

    local maxLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    maxLabel:SetPoint("TOPLEFT", 18, -60)
    maxLabel:SetWidth(200)
    maxLabel:SetJustifyH("LEFT")
    maxLabel:SetText("Maximum headlines bewaren (0 = onbeperkt)")
    maxLabel:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])

    local maxEdit = CreateFrame("EditBox", nil, frame, "InputBoxTemplate")
    maxEdit:SetSize(100, 24)
    maxEdit:SetPoint("TOPLEFT", maxLabel, "BOTTOMLEFT", 0, -8)
    maxEdit:SetNumeric(true)
    maxEdit:SetAutoFocus(false)
    styleEditBox(maxEdit)

    local pruneLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    pruneLabel:SetPoint("TOPLEFT", maxEdit, "BOTTOMLEFT", 0, -12)
    pruneLabel:SetWidth(200)
    pruneLabel:SetJustifyH("LEFT")
    pruneLabel:SetText("Verwijder headlines ouder dan dagen (0 = uit)")
    pruneLabel:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])

    local pruneEdit = CreateFrame("EditBox", nil, frame, "InputBoxTemplate")
    pruneEdit:SetSize(100, 24)
    pruneEdit:SetPoint("TOPLEFT", pruneLabel, "BOTTOMLEFT", 0, -8)
    pruneEdit:SetNumeric(true)
    pruneEdit:SetAutoFocus(false)
    styleEditBox(pruneEdit)

    local loginCheck = CreateFrame("CheckButton", nil, frame, "UICheckButtonTemplate")
    loginCheck:SetPoint("TOPLEFT", pruneEdit, "BOTTOMLEFT", -4, -12)
    loginCheck.text:SetText("Toon login hint in chat")
    styleCheckButton(loginCheck)

    local launcherScaleLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    launcherScaleLabel:SetPoint("TOPLEFT", loginCheck, "BOTTOMLEFT", 4, -10)
    launcherScaleLabel:SetText("Launcher schaal")
    launcherScaleLabel:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])

    local launcherScaleSlider = CreateFrame("Slider", "EKHLauncherScaleSlider", frame, "OptionsSliderTemplate")
    launcherScaleSlider:SetPoint("TOPLEFT", launcherScaleLabel, "BOTTOMLEFT", 4, -18)
    launcherScaleSlider:SetMinMaxValues(LAUNCHER_SCALE_MIN, LAUNCHER_SCALE_MAX)
    launcherScaleSlider:SetValueStep(LAUNCHER_SCALE_STEP)
    launcherScaleSlider:SetObeyStepOnDrag(true)
    launcherScaleSlider:SetWidth(170)

    local launcherScaleSliderLow = _G[launcherScaleSlider:GetName() .. "Low"]
    local launcherScaleSliderHigh = _G[launcherScaleSlider:GetName() .. "High"]
    local launcherScaleSliderText = _G[launcherScaleSlider:GetName() .. "Text"]
    if launcherScaleSliderLow then
        launcherScaleSliderLow:SetText(getLauncherScaleLabel(LAUNCHER_SCALE_MIN))
    end
    if launcherScaleSliderHigh then
        launcherScaleSliderHigh:SetText(getLauncherScaleLabel(LAUNCHER_SCALE_MAX))
    end
    if launcherScaleSliderText then
        launcherScaleSliderText:SetText("")
    end

    local launcherScaleValueText = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    launcherScaleValueText:SetPoint("LEFT", launcherScaleSlider, "RIGHT", 10, 0)
    launcherScaleValueText:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])
    launcherScaleValueText:SetText(getLauncherScaleLabel(LAUNCHER_SCALES[2]))

    local isUpdatingLauncherScaleSlider = false
    local function setLauncherScaleSliderValue(value)
        local normalized = normalizeLauncherScaleValue(value)
        ui.configLauncherScaleValue = normalized

        isUpdatingLauncherScaleSlider = true
        launcherScaleSlider:SetValue(normalized)
        isUpdatingLauncherScaleSlider = false

        launcherScaleValueText:SetText(getLauncherScaleLabel(normalized))
    end

    launcherScaleSlider:SetScript("OnValueChanged", function(self, value)
        if isUpdatingLauncherScaleSlider then
            return
        end

        local normalized = normalizeLauncherScaleValue(value)
        ui.configLauncherScaleValue = normalized
        launcherScaleValueText:SetText(getLauncherScaleLabel(normalized))

        if math.abs((self:GetValue() or 0) - normalized) > 0.0001 then
            isUpdatingLauncherScaleSlider = true
            self:SetValue(normalized)
            isUpdatingLauncherScaleSlider = false
        end

        setLauncherScale(normalized, false, false)
    end)
    setLauncherScaleSliderValue(LAUNCHER_SCALES[2])

    local fontTypeLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    fontTypeLabel:SetPoint("TOPLEFT", 248, -60)
    fontTypeLabel:SetText("Font type")
    fontTypeLabel:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])

    local fontTypeDropDown = CreateFrame("Frame", "EKHFontTypeDropdown", frame, "UIDropDownMenuTemplate")
    fontTypeDropDown:SetPoint("TOPLEFT", fontTypeLabel, "BOTTOMLEFT", -16, -2)
    UIDropDownMenu_SetWidth(fontTypeDropDown, 180)

    local fontSizeLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    fontSizeLabel:SetPoint("TOPLEFT", fontTypeDropDown, "BOTTOMLEFT", 20, -4)
    fontSizeLabel:SetWidth(180)
    fontSizeLabel:SetJustifyH("LEFT")
    fontSizeLabel:SetText("Font grootte (10-24)")
    fontSizeLabel:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])

    local fontSizeEdit = CreateFrame("EditBox", nil, frame, "InputBoxTemplate")
    fontSizeEdit:SetSize(100, 24)
    fontSizeEdit:SetPoint("TOPLEFT", fontSizeLabel, "BOTTOMLEFT", 0, -8)
    fontSizeEdit:SetNumeric(true)
    fontSizeEdit:SetAutoFocus(false)
    styleEditBox(fontSizeEdit)

    local commDivider = frame:CreateTexture(nil, "BORDER", nil, -1)
    commDivider:SetPoint("TOPLEFT", launcherScaleSlider, "BOTTOMLEFT", -4, -10)
    commDivider:SetPoint("RIGHT", frame, "RIGHT", -18, 0)
    commDivider:SetHeight(1)
    commDivider:SetTexture("Interface/Buttons/WHITE8x8")
    commDivider:SetVertexColor(ELV_BORDER[1], ELV_BORDER[2], ELV_BORDER[3], 0.75)

    local communicationSection = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    communicationSection:SetPoint("TOPLEFT", commDivider, "BOTTOMLEFT", 0, -8)
    communicationSection:SetText("Communicatie")
    communicationSection:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])

    local codeLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    codeLabel:SetPoint("TOPLEFT", communicationSection, "BOTTOMLEFT", 0, -8)
    codeLabel:SetText("Communicatiecode")
    codeLabel:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])

    local codeEdit = CreateFrame("EditBox", nil, frame, "InputBoxTemplate")
    codeEdit:SetSize(250, 24)
    codeEdit:SetPoint("TOPLEFT", codeLabel, "BOTTOMLEFT", 0, -8)
    codeEdit:SetAutoFocus(false)
    codeEdit:SetMaxLetters(48)
    styleEditBox(codeEdit)

    local generateCodeButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    generateCodeButton:SetSize(110, 24)
    generateCodeButton:SetPoint("LEFT", codeEdit, "RIGHT", 10, 0)
    generateCodeButton:SetText("Genereer")
    styleButton(generateCodeButton)

    local originalCodeValue = ""
    
    local function updateGenerateButtonState()
        local currentCode = codeEdit:GetText() or ""
        if currentCode ~= originalCodeValue and currentCode ~= "" then
            generateCodeButton:SetText("Opslaan")
        else
            generateCodeButton:SetText("Genereer")
        end
    end

    codeEdit:SetScript("OnTextChanged", function()
        updateGenerateButtonState()
    end)

    -- Store in ui table so refreshConfigFrame can access and update this
    ui.configCodeEditState = {
        getOriginal = function() return originalCodeValue end,
        setOriginal = function(value) originalCodeValue = value end,
        updateButtonState = updateGenerateButtonState,
    }

    local codeHint = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    codeHint:SetPoint("TOPLEFT", codeEdit, "BOTTOMLEFT", 2, -6)
    codeHint:SetWidth(420)
    codeHint:SetJustifyH("LEFT")
    codeHint:SetText("Alleen clients met dezelfde code synchroniseren met elkaar")
    codeHint:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])

    local discordUrlSection = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    discordUrlSection:SetPoint("TOPLEFT", codeHint, "BOTTOMLEFT", -2, -10)
    discordUrlSection:SetText("Discord")
    discordUrlSection:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])

    local discordUrlLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    discordUrlLabel:SetPoint("TOPLEFT", discordUrlSection, "BOTTOMLEFT", 2, -8)
    discordUrlLabel:SetText("Discord knop link")
    discordUrlLabel:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])

    local discordUrlEdit = CreateFrame("EditBox", nil, frame, "InputBoxTemplate")
    discordUrlEdit:SetSize(360, 24)
    discordUrlEdit:SetPoint("TOPLEFT", discordUrlLabel, "BOTTOMLEFT", 0, -8)
    discordUrlEdit:SetAutoFocus(false)
    discordUrlEdit:SetMaxLetters(220)
    styleEditBox(discordUrlEdit)

    local discordUrlHint = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    discordUrlHint:SetPoint("TOPLEFT", discordUrlEdit, "BOTTOMLEFT", 2, -6)
    discordUrlHint:SetWidth(420)
    discordUrlHint:SetJustifyH("LEFT")
    discordUrlHint:SetText("Alleen zichtbaar voor de guild master. Wordt gebruikt door de Discord afbeelding in het hoofdscherm.")
    discordUrlHint:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])

    local rankDivider = frame:CreateTexture(nil, "BORDER", nil, -1)
    rankDivider:SetPoint("TOPLEFT", discordUrlHint, "BOTTOMLEFT", -2, -14)
    rankDivider:SetPoint("RIGHT", frame, "RIGHT", -18, 0)
    rankDivider:SetHeight(1)
    rankDivider:SetTexture("Interface/Buttons/WHITE8x8")
    rankDivider:SetVertexColor(ELV_BORDER[1], ELV_BORDER[2], ELV_BORDER[3], 0.75)

    local rankSection = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    rankSection:SetPoint("TOPLEFT", rankDivider, "BOTTOMLEFT", 0, -8)
    rankSection:SetText("Postrechten")
    rankSection:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])

    local rankHint = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    rankHint:SetPoint("TOPLEFT", rankSection, "BOTTOMLEFT", 0, -8)
    rankHint:SetWidth(420)
    rankHint:SetJustifyH("LEFT")
    rankHint:SetText("Alleen zichtbaar voor de guild master. Geselecteerde rangen mogen headlines plaatsen.")
    rankHint:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])

    local masterHint = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    masterHint:SetPoint("TOPLEFT", rankHint, "BOTTOMLEFT", 0, -10)
    masterHint:SetText("Guild master heeft altijd postrechten.")
    masterHint:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])

    local rankChecks = {}
    local guildRanks = NS.GetGuildRanks and NS.GetGuildRanks() or {}
    for _, rankInfo in ipairs(guildRanks) do
        if rankInfo.rankIndex ~= 0 then
            local check = CreateFrame("CheckButton", nil, frame, "UICheckButtonTemplate")
            local column = (#rankChecks % 2)
            local row = math.floor(#rankChecks / 2)
            check:SetPoint("TOPLEFT", masterHint, "BOTTOMLEFT", (column * 200) - 4, CONFIG_RANK_CHECK_BASE_OFFSET - (row * 28))
            check.text:SetText(rankInfo.label or ("Rang " .. tostring(rankInfo.rankIndex)))
            styleCheckButton(check)

            table.insert(rankChecks, {
                check = check,
                rankIndex = rankInfo.rankIndex,
            })
        end
    end

    local function getRankListBottomOffset(totalRows)
        return CONFIG_RANK_CHECK_BASE_OFFSET - (totalRows * CONFIG_FRAME_EXTRA_ROW_HEIGHT) - 6
    end

    local initialRankRows = math.max(1, math.ceil((#rankChecks) / 2))

    local deleteRankDivider = frame:CreateTexture(nil, "BORDER", nil, -1)
    deleteRankDivider:SetPoint("TOPLEFT", masterHint, "BOTTOMLEFT", -2, getRankListBottomOffset(initialRankRows))
    deleteRankDivider:SetPoint("RIGHT", frame, "RIGHT", -18, 0)
    deleteRankDivider:SetHeight(1)
    deleteRankDivider:SetTexture("Interface/Buttons/WHITE8x8")
    deleteRankDivider:SetVertexColor(ELV_BORDER[1], ELV_BORDER[2], ELV_BORDER[3], 0.75)

    local deleteRankSection = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    deleteRankSection:SetPoint("TOPLEFT", deleteRankDivider, "BOTTOMLEFT", 0, -8)
    deleteRankSection:SetText("Verwijderrechten")
    deleteRankSection:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])

    local deleteRankHint = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    deleteRankHint:SetPoint("TOPLEFT", deleteRankSection, "BOTTOMLEFT", 0, -8)
    deleteRankHint:SetWidth(420)
    deleteRankHint:SetJustifyH("LEFT")
    deleteRankHint:SetText("Guild master en auteur mogen altijd verwijderen. Geselecteerde rangen krijgen extra verwijderrechten.")
    deleteRankHint:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])

    local deleteMasterHint = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    deleteMasterHint:SetPoint("TOPLEFT", deleteRankHint, "BOTTOMLEFT", 0, -10)
    deleteMasterHint:SetText("Auteur en guild master behouden altijd verwijderrechten.")
    deleteMasterHint:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])

    local deleteRankChecks = {}
    for _, rankInfo in ipairs(guildRanks) do
        if rankInfo.rankIndex ~= 0 then
            local check = CreateFrame("CheckButton", nil, frame, "UICheckButtonTemplate")
            local column = (#deleteRankChecks % 2)
            local row = math.floor(#deleteRankChecks / 2)
            check:SetPoint("TOPLEFT", deleteMasterHint, "BOTTOMLEFT", (column * 200) - 4, CONFIG_RANK_CHECK_BASE_OFFSET - (row * 28))
            check.text:SetText(rankInfo.label or ("Rang " .. tostring(rankInfo.rankIndex)))
            styleCheckButton(check)

            table.insert(deleteRankChecks, {
                check = check,
                rankIndex = rankInfo.rankIndex,
            })
        end
    end

    local initialDeleteRows = math.max(1, math.ceil((#deleteRankChecks) / 2))

    local commViewRankDivider = frame:CreateTexture(nil, "BORDER", nil, -1)
    commViewRankDivider:SetPoint("TOPLEFT", deleteMasterHint, "BOTTOMLEFT", -2, getRankListBottomOffset(initialDeleteRows))
    commViewRankDivider:SetPoint("RIGHT", frame, "RIGHT", -18, 0)
    commViewRankDivider:SetHeight(1)
    commViewRankDivider:SetTexture("Interface/Buttons/WHITE8x8")
    commViewRankDivider:SetVertexColor(ELV_BORDER[1], ELV_BORDER[2], ELV_BORDER[3], 0.75)

    local commViewRankSection = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    commViewRankSection:SetPoint("TOPLEFT", commViewRankDivider, "BOTTOMLEFT", 0, -8)
    commViewRankSection:SetText("Communicatieoverzicht")
    commViewRankSection:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])

    local commViewRankHint = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    commViewRankHint:SetPoint("TOPLEFT", commViewRankSection, "BOTTOMLEFT", 0, -8)
    commViewRankHint:SetWidth(420)
    commViewRankHint:SetJustifyH("LEFT")
    commViewRankHint:SetText("Guild master ziet dit altijd. Geselecteerde rangen mogen ook de lijst met communicerende guildleden zien.")
    commViewRankHint:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])

    local commViewMasterHint = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    commViewMasterHint:SetPoint("TOPLEFT", commViewRankHint, "BOTTOMLEFT", 0, -10)
    commViewMasterHint:SetText("Guild master behoudt altijd toegang tot dit overzicht.")
    commViewMasterHint:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])

    local commViewRankChecks = {}
    for _, rankInfo in ipairs(guildRanks) do
        if rankInfo.rankIndex ~= 0 then
            local check = CreateFrame("CheckButton", nil, frame, "UICheckButtonTemplate")
            local column = (#commViewRankChecks % 2)
            local row = math.floor(#commViewRankChecks / 2)
            check:SetPoint("TOPLEFT", commViewMasterHint, "BOTTOMLEFT", (column * 200) - 4, CONFIG_RANK_CHECK_BASE_OFFSET - (row * 28))
            check.text:SetText(rankInfo.label or ("Rang " .. tostring(rankInfo.rankIndex)))
            styleCheckButton(check)

            table.insert(commViewRankChecks, {
                check = check,
                rankIndex = rankInfo.rankIndex,
            })
        end
    end

    ui.configFontTypeOptions = buildFontTypeOptions()

    local function setFontTypeDropdownValue(value)
        local normalized = normalizeFontType(value)
        ui.configFontTypeValue = normalized
        UIDropDownMenu_SetText(fontTypeDropDown, getFontTypeLabel(normalized))
    end

    local function refreshFontTypeDropdownOptions()
        ui.configFontTypeOptions = buildFontTypeOptions()

        local selected = normalizeFontType(ui.configFontTypeValue)
        local found = false
        for _, option in ipairs(ui.configFontTypeOptions) do
            if option.value == selected then
                found = true
                break
            end
        end

        if not found then
            selected = "frizqt"
        end

        setFontTypeDropdownValue(selected)
    end

    UIDropDownMenu_Initialize(fontTypeDropDown, function(_, _, _)
        for _, option in ipairs(ui.configFontTypeOptions or {}) do
            local info = UIDropDownMenu_CreateInfo()
            info.text = option.label
            info.value = option.value
            info.checked = (ui.configFontTypeValue == option.value)
            info.func = function()
                setFontTypeDropdownValue(option.value)
            end
            UIDropDownMenu_AddButton(info)
        end
    end)
    refreshFontTypeDropdownOptions()


    maxEdit:SetScript("OnEnterPressed", function()
        pruneEdit:SetFocus()
    end)
    pruneEdit:SetScript("OnEnterPressed", function()
        codeEdit:SetFocus()
    end)
    codeEdit:SetScript("OnEnterPressed", function()
        if discordUrlEdit:IsShown() then
            discordUrlEdit:SetFocus()
        else
            fontSizeEdit:SetFocus()
        end
    end)
    discordUrlEdit:SetScript("OnEnterPressed", function()
        fontSizeEdit:SetFocus()
    end)

    attachTabNavigation({ maxEdit, pruneEdit, codeEdit, discordUrlEdit, fontSizeEdit })

    local saveButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    saveButton:SetSize(110, 24)
    saveButton:SetPoint("BOTTOMRIGHT", -16, 16)
    saveButton:SetText("Opslaan")
    styleButton(saveButton)

    local cancelButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    cancelButton:SetSize(110, 24)
    cancelButton:SetPoint("RIGHT", saveButton, "LEFT", -8, 0)
    cancelButton:SetText("Sluiten")
    styleButton(cancelButton)
    cancelButton:SetScript("OnClick", function()
        frame:Hide()
    end)

    local info = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    info:SetPoint("BOTTOMLEFT", 18, 50)
    info:SetWidth(300)
    info:SetJustifyH("LEFT")
    info:SetText("Instellingen zijn lokaal. Deel je communicatiecode met andere clients.")
    info:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])

    local commStatus = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    commStatus:SetPoint("BOTTOMLEFT", 18, 76)
    commStatus:SetWidth(420)
    commStatus:SetJustifyH("LEFT")
    commStatus:SetWordWrap(false)
    commStatus:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])
    commStatus:SetText("Guild online: 0 | Communicerende clients: 0")

    ui.configFrame = frame
    ui.configMaxEdit = maxEdit
    ui.configPruneEdit = pruneEdit
    ui.configLoginCheck = loginCheck
    ui.configCodeEdit = codeEdit
    ui.configFontTypeDropDown = fontTypeDropDown
    ui.configSetFontTypeValue = setFontTypeDropdownValue
    ui.configRefreshFontTypeOptions = refreshFontTypeDropdownOptions
    ui.configFontSizeEdit = fontSizeEdit
    ui.configLauncherScaleSlider = launcherScaleSlider
    ui.configSetLauncherScaleValue = setLauncherScaleSliderValue
    ui.configCommStatus = commStatus
    ui.configDiscordUrlSection = discordUrlSection
    ui.configDiscordUrlLabel = discordUrlLabel
    ui.configDiscordUrlEdit = discordUrlEdit
    ui.configDiscordUrlHint = discordUrlHint
    ui.configRankDivider = rankDivider
    ui.configRankSection = rankSection
    ui.configRankHint = rankHint
    ui.configRankMasterHint = masterHint
    ui.configRankChecks = rankChecks
    ui.configDeleteRankDivider = deleteRankDivider
    ui.configDeleteRankSection = deleteRankSection
    ui.configDeleteRankHint = deleteRankHint
    ui.configDeleteRankMasterHint = deleteMasterHint
    ui.configDeleteRankChecks = deleteRankChecks
    ui.configCommViewRankDivider = commViewRankDivider
    ui.configCommViewRankSection = commViewRankSection
    ui.configCommViewRankHint = commViewRankHint
    ui.configCommViewRankMasterHint = commViewMasterHint
    ui.configCommViewRankChecks = commViewRankChecks

    generateCodeButton:SetScript("OnClick", function()
        local currentCode = codeEdit:GetText() or ""
        local originalValue = ui.configCodeEditState and ui.configCodeEditState.getOriginal() or ""
        
        if currentCode ~= originalValue and currentCode ~= "" then
            -- Save mode: update the configuration
            if NS.UpdateConfig then
                NS.UpdateConfig({
                    communicationCode = currentCode,
                })
                if ui.configCodeEditState then
                    ui.configCodeEditState.setOriginal(currentCode)
                    ui.configCodeEditState.updateButtonState()
                end
            end
        else
            -- Generate mode: generate a new code
            local generated = NS.GenerateCommunicationCode and NS.GenerateCommunicationCode() or ""
            if generated ~= "" then
                codeEdit:SetText(generated)
                codeEdit:HighlightText()
            end
        end
    end)

    saveButton:SetScript("OnClick", function()
        local maxValue = tonumber(maxEdit:GetText() or "") or 50
        local pruneValue = tonumber(pruneEdit:GetText() or "") or 30
        local communicationCodeValue = codeEdit:GetText() or ""
        local fontTypeValue = ui.configFontTypeValue or "frizqt"
        local fontSizeValue = tonumber(fontSizeEdit:GetText() or "") or 14
        local launcherScaleValue = ui.configLauncherScaleValue or LAUNCHER_SCALES[2]
        local allowedPostRanksValue = nil
        local allowedDeleteRanksValue = nil
        local allowedCommunicationViewRanksValue = nil
        local discordHeaderUrlValue = nil

        if NS.IsLocalGuildMaster and NS.IsLocalGuildMaster() then
            allowedPostRanksValue = {}
            for _, rankInfo in ipairs(ui.configRankChecks or {}) do
                if rankInfo.check:GetChecked() then
                    table.insert(allowedPostRanksValue, rankInfo.rankIndex)
                end
            end

            allowedDeleteRanksValue = {}
            for _, rankInfo in ipairs(ui.configDeleteRankChecks or {}) do
                if rankInfo.check:GetChecked() then
                    table.insert(allowedDeleteRanksValue, rankInfo.rankIndex)
                end
            end

            allowedCommunicationViewRanksValue = {}
            for _, rankInfo in ipairs(ui.configCommViewRankChecks or {}) do
                if rankInfo.check:GetChecked() then
                    table.insert(allowedCommunicationViewRanksValue, rankInfo.rankIndex)
                end
            end

            discordHeaderUrlValue = discordUrlEdit:GetText() or ""
        end

        local cfg = NS.UpdateConfig({
            maxPosts = maxValue,
            pruneDays = pruneValue,
            showLoginMessage = loginCheck:GetChecked() and true or false,
            allowedPostRanks = allowedPostRanksValue,
            allowedDeleteRanks = allowedDeleteRanksValue,
            allowedCommunicationViewRanks = allowedCommunicationViewRanksValue,
            communicationCode = communicationCodeValue,
            fontType = fontTypeValue,
            fontSize = fontSizeValue,
            launcherScale = launcherScaleValue,
            discordHeaderUrl = discordHeaderUrlValue,
        })

        maxEdit:SetText(tostring(cfg.maxPosts))
        pruneEdit:SetText(tostring(cfg.pruneDays))
        loginCheck:SetChecked(cfg.showLoginMessage)
        codeEdit:SetText(cfg.communicationCode or "")
        
        -- Reset the original code value after saving
        if ui.configCodeEditState then
            ui.configCodeEditState.setOriginal(cfg.communicationCode or "")
            ui.configCodeEditState.updateButtonState()
        end
        
        if ui.configSetFontTypeValue then
            ui.configSetFontTypeValue(cfg.fontType or "frizqt")
        end
        fontSizeEdit:SetText(tostring(cfg.fontSize or 14))
        if ui.configSetLauncherScaleValue then
            ui.configSetLauncherScaleValue(cfg.launcherScale or LAUNCHER_SCALES[2])
        end
        setLauncherScale(cfg.launcherScale or LAUNCHER_SCALES[2], false, false)
        discordUrlEdit:SetText(cfg.discordHeaderUrl or DEFAULT_DISCORD_URL)

        local allowedLookup = {}
        for _, rankIndex in ipairs(cfg.allowedPostRanks or {}) do
            allowedLookup[rankIndex] = true
        end

        local allowedDeleteLookup = {}
        for _, rankIndex in ipairs(cfg.allowedDeleteRanks or {}) do
            allowedDeleteLookup[rankIndex] = true
        end

        local allowedCommunicationViewLookup = {}
        for _, rankIndex in ipairs(cfg.allowedCommunicationViewRanks or {}) do
            allowedCommunicationViewLookup[rankIndex] = true
        end

        for _, rankInfo in ipairs(ui.configRankChecks or {}) do
            rankInfo.check:SetChecked(allowedLookup[rankInfo.rankIndex] == true)
        end
        for _, rankInfo in ipairs(ui.configDeleteRankChecks or {}) do
            rankInfo.check:SetChecked(allowedDeleteLookup[rankInfo.rankIndex] == true)
        end
        for _, rankInfo in ipairs(ui.configCommViewRankChecks or {}) do
            rankInfo.check:SetChecked(allowedCommunicationViewLookup[rankInfo.rankIndex] == true)
        end

        if NS.RefreshUI then
            NS.RefreshUI()
        end

        UIErrorsFrame:AddMessage("Instellingen opgeslagen.", 0.2, 1, 0.2)
    end)

    fontSizeEdit:SetScript("OnEnterPressed", function()
        saveButton:Click()
    end)

    return frame
end

local function clampFrameToViewport(frame)
    if not frame or not UIParent then
        return
    end

    local pLeft = UIParent:GetLeft()
    local pRight = UIParent:GetRight()
    local pTop = UIParent:GetTop()
    local pBottom = UIParent:GetBottom()
    local centerX, centerY = frame:GetCenter()
    if not pLeft or not pRight or not pTop or not pBottom or not centerX or not centerY then
        return
    end

    local margin = 14
    local halfW = ((frame:GetWidth() or 0) * (frame:GetEffectiveScale() or 1)) / 2
    local halfH = ((frame:GetHeight() or 0) * (frame:GetEffectiveScale() or 1)) / 2

    local minX = pLeft + halfW + margin
    local maxX = pRight - halfW - margin
    local minY = pBottom + halfH + margin
    local maxY = pTop - halfH - margin

    local newX = centerX
    local newY = centerY

    if minX > maxX then
        newX = (pLeft + pRight) / 2
    else
        if newX < minX then
            newX = minX
        elseif newX > maxX then
            newX = maxX
        end
    end

    if minY > maxY then
        newY = (pBottom + pTop) / 2
    else
        if newY < minY then
            newY = minY
        elseif newY > maxY then
            newY = maxY
        end
    end

    local parentScale = UIParent:GetEffectiveScale() or 1
    local relX = newX / parentScale
    local relY = newY / parentScale

    frame:ClearAllPoints()
    frame:SetPoint("CENTER", UIParent, "BOTTOMLEFT", relX, relY)
end

local function refreshConfigFrame()
    if not ui.configFrame then
        return
    end

    if ui.configRefreshFontTypeOptions then
        ui.configRefreshFontTypeOptions()
    end

    local cfg = NS.GetConfig and NS.GetConfig() or {
        maxPosts = 0,
        pruneDays = 0,
        showLoginMessage = true,
        communicationCode = "eerstkoffie",
        fontType = "frizqt",
        fontSize = 14,
        launcherScale = LAUNCHER_SCALES[2],
        allowedPostRanks = { 1 },
        allowedDeleteRanks = {},
        allowedCommunicationViewRanks = {},
        discordHeaderUrl = DEFAULT_DISCORD_URL,
    }

    ui.configMaxEdit:SetText(tostring(cfg.maxPosts))
    ui.configPruneEdit:SetText(tostring(cfg.pruneDays))
    ui.configLoginCheck:SetChecked(cfg.showLoginMessage)
    ui.configCodeEdit:SetText(cfg.communicationCode or "")
    
    -- Reset the original code value when config frame is refreshed
    if ui.configCodeEditState then
        ui.configCodeEditState.setOriginal(cfg.communicationCode or "")
        ui.configCodeEditState.updateButtonState()
    end
    
    if ui.configSetFontTypeValue then
        ui.configSetFontTypeValue(cfg.fontType or "frizqt")
    end
    ui.configFontSizeEdit:SetText(tostring(cfg.fontSize or 14))
    if ui.configSetLauncherScaleValue then
        ui.configSetLauncherScaleValue(cfg.launcherScale or LAUNCHER_SCALES[2])
    end
    setLauncherScale(cfg.launcherScale or LAUNCHER_SCALES[2], false, false)
    if ui.configDiscordUrlEdit then
        ui.configDiscordUrlEdit:SetText(cfg.discordHeaderUrl or DEFAULT_DISCORD_URL)
    end

    refreshCommunicationStatusTexts()

    local isGuildMaster = NS.IsLocalGuildMaster and NS.IsLocalGuildMaster() or false

    local allowedLookup = {}
    for _, rankIndex in ipairs(cfg.allowedPostRanks or {}) do
        allowedLookup[rankIndex] = true
    end

    local allowedDeleteLookup = {}
    for _, rankIndex in ipairs(cfg.allowedDeleteRanks or {}) do
        allowedDeleteLookup[rankIndex] = true
    end

    local allowedCommunicationViewLookup = {}
    for _, rankIndex in ipairs(cfg.allowedCommunicationViewRanks or {}) do
        allowedCommunicationViewLookup[rankIndex] = true
    end

    local guildRanks = NS.GetGuildRanks and NS.GetGuildRanks() or {}
    local existingChecks = ui.configRankChecks or {}
    local existingDeleteChecks = ui.configDeleteRankChecks or {}
    local existingCommViewChecks = ui.configCommViewRankChecks or {}
    local existingByRank = {}
    local existingDeleteByRank = {}
    local existingCommViewByRank = {}

    for _, rankInfo in ipairs(existingChecks) do
        existingByRank[rankInfo.rankIndex] = rankInfo.check
        rankInfo.check:Hide()
    end
    for _, rankInfo in ipairs(existingDeleteChecks) do
        existingDeleteByRank[rankInfo.rankIndex] = rankInfo.check
        rankInfo.check:Hide()
    end
    for _, rankInfo in ipairs(existingCommViewChecks) do
        existingCommViewByRank[rankInfo.rankIndex] = rankInfo.check
        rankInfo.check:Hide()
    end

    ui.configRankChecks = {}
    ui.configDeleteRankChecks = {}
    ui.configCommViewRankChecks = {}
    local visibleIndex = 0
    local visibleDeleteIndex = 0
    local visibleCommViewIndex = 0

    for _, rankInfo in ipairs(guildRanks) do
        if rankInfo.rankIndex ~= 0 then
            local check = existingByRank[rankInfo.rankIndex]
            if not check then
                check = CreateFrame("CheckButton", nil, ui.configFrame, "UICheckButtonTemplate")
                styleCheckButton(check)
            end

            local column = visibleIndex % 2
            local row = math.floor(visibleIndex / 2)
            check:ClearAllPoints()
            check:SetPoint("TOPLEFT", ui.configRankMasterHint, "BOTTOMLEFT", (column * 200) - 4, CONFIG_RANK_CHECK_BASE_OFFSET - (row * 28))
            check.text:SetText(rankInfo.label or ("Rang " .. tostring(rankInfo.rankIndex)))
            check:SetChecked(allowedLookup[rankInfo.rankIndex] == true)
            table.insert(ui.configRankChecks, {
                check = check,
                rankIndex = rankInfo.rankIndex,
            })
            visibleIndex = visibleIndex + 1

            local deleteCheck = existingDeleteByRank[rankInfo.rankIndex]
            if not deleteCheck then
                deleteCheck = CreateFrame("CheckButton", nil, ui.configFrame, "UICheckButtonTemplate")
                styleCheckButton(deleteCheck)
            end

            local deleteColumn = visibleDeleteIndex % 2
            local deleteRow = math.floor(visibleDeleteIndex / 2)
            deleteCheck:ClearAllPoints()
            deleteCheck:SetPoint("TOPLEFT", ui.configDeleteRankMasterHint, "BOTTOMLEFT", (deleteColumn * 200) - 4, CONFIG_RANK_CHECK_BASE_OFFSET - (deleteRow * 28))
            deleteCheck.text:SetText(rankInfo.label or ("Rang " .. tostring(rankInfo.rankIndex)))
            deleteCheck:SetChecked(allowedDeleteLookup[rankInfo.rankIndex] == true)
            table.insert(ui.configDeleteRankChecks, {
                check = deleteCheck,
                rankIndex = rankInfo.rankIndex,
            })
            visibleDeleteIndex = visibleDeleteIndex + 1

            local commViewCheck = existingCommViewByRank[rankInfo.rankIndex]
            if not commViewCheck then
                commViewCheck = CreateFrame("CheckButton", nil, ui.configFrame, "UICheckButtonTemplate")
                styleCheckButton(commViewCheck)
            end

            local commViewColumn = visibleCommViewIndex % 2
            local commViewRow = math.floor(visibleCommViewIndex / 2)
            commViewCheck:ClearAllPoints()
            commViewCheck:SetPoint("TOPLEFT", ui.configCommViewRankMasterHint, "BOTTOMLEFT", (commViewColumn * 200) - 4, CONFIG_RANK_CHECK_BASE_OFFSET - (commViewRow * 28))
            commViewCheck.text:SetText(rankInfo.label or ("Rang " .. tostring(rankInfo.rankIndex)))
            commViewCheck:SetChecked(allowedCommunicationViewLookup[rankInfo.rankIndex] == true)
            table.insert(ui.configCommViewRankChecks, {
                check = commViewCheck,
                rankIndex = rankInfo.rankIndex,
            })
            visibleCommViewIndex = visibleCommViewIndex + 1
        end
    end

    local totalRows = math.max(1, math.ceil((#(ui.configRankChecks or {})) / 2))
    local deleteDividerOffset = CONFIG_RANK_CHECK_BASE_OFFSET - (totalRows * CONFIG_FRAME_EXTRA_ROW_HEIGHT) - 6
    if ui.configDeleteRankDivider then
        ui.configDeleteRankDivider:ClearAllPoints()
        ui.configDeleteRankDivider:SetPoint("TOPLEFT", ui.configRankMasterHint, "BOTTOMLEFT", -2, deleteDividerOffset)
        ui.configDeleteRankDivider:SetPoint("RIGHT", ui.configFrame, "RIGHT", -18, 0)
    end

    local totalDeleteRows = math.max(1, math.ceil((#(ui.configDeleteRankChecks or {})) / 2))
    local commViewDividerOffset = CONFIG_RANK_CHECK_BASE_OFFSET - (totalDeleteRows * CONFIG_FRAME_EXTRA_ROW_HEIGHT) - 6
    if ui.configCommViewRankDivider then
        ui.configCommViewRankDivider:ClearAllPoints()
        ui.configCommViewRankDivider:SetPoint("TOPLEFT", ui.configDeleteRankMasterHint, "BOTTOMLEFT", -2, commViewDividerOffset)
        ui.configCommViewRankDivider:SetPoint("RIGHT", ui.configFrame, "RIGHT", -18, 0)
    end

    local rankWidgets = {
        ui.configDiscordUrlSection,
        ui.configDiscordUrlLabel,
        ui.configDiscordUrlEdit,
        ui.configDiscordUrlHint,
        ui.configRankDivider,
        ui.configRankSection,
        ui.configRankHint,
        ui.configRankMasterHint,
        ui.configDeleteRankDivider,
        ui.configDeleteRankSection,
        ui.configDeleteRankHint,
        ui.configDeleteRankMasterHint,
        ui.configCommViewRankDivider,
        ui.configCommViewRankSection,
        ui.configCommViewRankHint,
        ui.configCommViewRankMasterHint,
    }

    for _, widget in ipairs(rankWidgets) do
        if widget then
            if isGuildMaster then
                widget:Show()
            else
                widget:Hide()
            end
        end
    end

    for _, rankInfo in ipairs(ui.configRankChecks or {}) do
        rankInfo.check:SetChecked(allowedLookup[rankInfo.rankIndex] == true)
        if isGuildMaster then
            rankInfo.check:Show()
        else
            rankInfo.check:Hide()
        end
    end

    for _, rankInfo in ipairs(ui.configDeleteRankChecks or {}) do
        rankInfo.check:SetChecked(allowedDeleteLookup[rankInfo.rankIndex] == true)
        if isGuildMaster then
            rankInfo.check:Show()
        else
            rankInfo.check:Hide()
        end
    end

    for _, rankInfo in ipairs(ui.configCommViewRankChecks or {}) do
        rankInfo.check:SetChecked(allowedCommunicationViewLookup[rankInfo.rankIndex] == true)
        if isGuildMaster then
            rankInfo.check:Show()
        else
            rankInfo.check:Hide()
        end
    end

    local targetHeight = CONFIG_FRAME_NON_GM_HEIGHT + 12
    if isGuildMaster then
        local rankRows = math.max(1, math.ceil((#(ui.configRankChecks or {})) / 2))
        local deleteRows = math.max(1, math.ceil((#(ui.configDeleteRankChecks or {})) / 2))
        local commViewRows = math.max(1, math.ceil((#(ui.configCommViewRankChecks or {})) / 2))

        targetHeight = CONFIG_FRAME_GM_BASE_HEIGHT
            + CONFIG_DELETE_PERMISSIONS_EXTRA_HEIGHT
            + CONFIG_COMM_VIEW_PERMISSIONS_EXTRA_HEIGHT
            + ((rankRows + deleteRows + commViewRows) * CONFIG_FRAME_EXTRA_ROW_HEIGHT)
            + CONFIG_FRAME_BOTTOM_SAFE_HEIGHT
    end

    if ui.configFrame and ui.configFrame:GetHeight() ~= targetHeight then
        ui.configFrame:SetHeight(targetHeight)
        if ui.configFrame:IsShown() then
            clampFrameToViewport(ui.configFrame)
        end
    end
end

local function createMainFrame()
    if ui.frame then
        return ui.frame
    end

    local frame = CreateFrame("Frame", "EKHMainFrame", UIParent, "BackdropTemplate")
    local cfg = NS.GetConfig and NS.GetConfig() or { mainFrameWidth = 860, mainFrameHeight = 620, mainFrameScale = 1 }
    local frameWidth = math.max(MAIN_FRAME_MIN_WIDTH, tonumber(cfg.mainFrameWidth) or 860)
    local frameHeight = math.max(MAIN_FRAME_MIN_HEIGHT, tonumber(cfg.mainFrameHeight) or 620)
    local frameScale = tonumber(cfg.mainFrameScale) or 1
    registerEscClosableFrame("EKHMainFrame")
    frame:SetSize(frameWidth, frameHeight)
    frame:SetScale(frameScale)
    frame:SetPoint("CENTER")
    frame:SetFrameStrata("HIGH")
    frame:SetMovable(true)
    frame:SetResizable(true)
    if frame.SetMinResize then
        frame:SetMinResize(MAIN_FRAME_MIN_WIDTH, MAIN_FRAME_MIN_HEIGHT)
    end
    frame:EnableMouse(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetScript("OnDragStart", frame.StartMoving)
    frame:SetScript("OnDragStop", frame.StopMovingOrSizing)
    frame:SetScript("OnShow", function()
        updateLauncherVisualState()
    end)
    frame:SetScript("OnHide", function()
        if NS.UpdateConfig then
            NS.UpdateConfig({
                mainFrameWidth = frame:GetWidth() or 860,
                mainFrameHeight = frame:GetHeight() or 620,
                mainFrameScale = frame:GetScale() or 1,
            })
        end
        ui.launcherHiddenUntil = nil
        updateLauncherVisualState()
    end)
    styleBackdrop(frame)
    frame:SetBackdropColor(ELV_BG[1], ELV_BG[2], ELV_BG[3], ELV_BG[4])
    addAbstractLayers(frame)
    frame:Hide()

    local logoFrame = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    logoFrame:SetSize(440, 110)
    logoFrame:SetPoint("TOPLEFT", 14, -12)
    styleBackdrop(logoFrame)
    logoFrame:SetBackdropColor(ELV_PANEL[1], ELV_PANEL[2], ELV_PANEL[3], ELV_PANEL[4])

    local logoTexture = logoFrame:CreateTexture(nil, "ARTWORK")
    logoTexture:SetSize(430, 100)
    logoTexture:SetPoint("CENTER")
    logoTexture:SetTexture(HEADER_LOGO_TEXTURE)
    logoTexture:SetTexCoord(0, 1, 0, 1)

    local logoFallback = logoFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    logoFallback:SetPoint("CENTER")
    logoFallback:SetText("EK")
    logoFallback:SetTextColor(0.8, 0.66, 0.4)
    logoFallback:Hide()

    if not logoTexture:GetTexture() then
        logoFallback:Show()
    end

    local logoGlow = logoFrame:CreateTexture(nil, "OVERLAY")
    logoGlow:SetPoint("TOPLEFT", -6, 6)
    logoGlow:SetPoint("BOTTOMRIGHT", 6, -6)
    logoGlow:SetTexture("Interface/Buttons/WHITE8x8")
    logoGlow:SetVertexColor(ELV_ACCENT[1], ELV_ACCENT[2], ELV_ACCENT[3], 0)

    local pulse = logoGlow:CreateAnimationGroup()
    local fadeIn = pulse:CreateAnimation("Alpha")
    fadeIn:SetOrder(1)
    fadeIn:SetFromAlpha(0)
    fadeIn:SetToAlpha(0.4)
    fadeIn:SetDuration(0.16)

    local fadeOut = pulse:CreateAnimation("Alpha")
    fadeOut:SetOrder(2)
    fadeOut:SetFromAlpha(0.4)
    fadeOut:SetToAlpha(0)
    fadeOut:SetDuration(0.5)

    pulse:SetLooping("REPEAT")
    pulse:SetScript("OnPlay", function()
        ui._logoPulseCycles = 0
    end)
    pulse:SetScript("OnLoop", function(group)
        ui._logoPulseCycles = (ui._logoPulseCycles or 0) + 1
        if ui._logoPulseCycles >= 2 then
            group:Stop()
        end
    end)

    ui.logoPulseAnim = pulse
    ui.logoGlow = logoGlow

    local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", logoFrame, "BOTTOMLEFT", 0, -8)
    title:SetText("Eerst Koffie Headlines")
    title:SetTextColor(FONT_TITLE[1], FONT_TITLE[2], FONT_TITLE[3])
    title:Hide()

    local subtitle = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    subtitle:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 2, -7)
    subtitle:SetText("Bridge tussen guild en Discord headlines")
    subtitle:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])
    subtitle:Hide()

    local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", -6, -6)
    styleCloseButton(close)

    local maximizeBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    maximizeBtn:SetSize(28, 22)
    maximizeBtn:SetPoint("RIGHT", close, "LEFT", -8, -1)
    maximizeBtn:SetText("□")
    styleButton(maximizeBtn)

    local discordHeaderButton = CreateFrame("Button", nil, frame, "BackdropTemplate")
    discordHeaderButton:SetSize(240, 60)
    discordHeaderButton:SetPoint("TOPRIGHT", -34, -66)
    styleBackdrop(discordHeaderButton)
    discordHeaderButton:SetBackdropColor(1, 1, 1, 1)

    local discordTexture = discordHeaderButton:CreateTexture(nil, "ARTWORK")
    discordTexture:SetPoint("TOPLEFT", 4, -4)
    discordTexture:SetPoint("BOTTOMRIGHT", -4, 4)
    discordTexture:SetTexture(DISCORD_BUTTON_TEXTURE)

    local discordFallback = discordHeaderButton:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    discordFallback:SetPoint("CENTER")
    discordFallback:SetText("JOIN DISCORD")
    discordFallback:SetTextColor(0.55, 0.67, 0.95)
    discordFallback:Hide()

    if not discordTexture:GetTexture() then
        discordFallback:Show()
    end

    discordHeaderButton:SetScript("OnClick", function()
        showCopyDialog(getHeaderDiscordUrl())
        UIErrorsFrame:AddMessage("Discord link klaar om te kopieren (Ctrl+C).", 0.45, 0.85, 1)
    end)

    local mainCommStatus = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    mainCommStatus:SetPoint("BOTTOMRIGHT", discordHeaderButton, "TOPRIGHT", 0, 8)
    mainCommStatus:SetWidth(420)
    mainCommStatus:SetJustifyH("RIGHT")
    mainCommStatus:SetWordWrap(false)
    mainCommStatus:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])
    mainCommStatus:SetText("Guild online: 0 | Communicerende clients: 0")

    local configBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    configBtn:SetSize(110, 24)
    configBtn:SetPoint("TOPRIGHT", -36, -140)
    configBtn:SetText("Instellingen")
    styleButton(configBtn)
    configBtn:SetScript("OnClick", function()
        if NS.ToggleConfigFrame then
            NS.ToggleConfigFrame()
        end
    end)

    local resetFilterButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    resetFilterButton:SetSize(108, 24)
    resetFilterButton:SetPoint("RIGHT", configBtn, "LEFT", -8, 0)
    resetFilterButton:SetText("Reset Filter")
    styleButton(resetFilterButton)

    local searchButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    searchButton:SetSize(82, 24)
    searchButton:SetPoint("RIGHT", resetFilterButton, "LEFT", -8, 0)
    searchButton:SetText("Zoek")
    styleButton(searchButton)

    local searchEdit = CreateFrame("EditBox", nil, frame, "InputBoxTemplate")
    searchEdit:SetSize(220, 24)
    searchEdit:SetPoint("RIGHT", searchButton, "LEFT", -8, 0)
    searchEdit:SetAutoFocus(false)
    searchEdit:SetMaxLetters(120)
    searchEdit:SetScript("OnEscapePressed", searchEdit.ClearFocus)
    styleEditBox(searchEdit)

    local headerDividerTop = frame:CreateTexture(nil, "BORDER", nil, -1)
    headerDividerTop:SetPoint("LEFT", logoFrame, "RIGHT", 18, 0)
    headerDividerTop:SetPoint("RIGHT", discordHeaderButton, "LEFT", -18, 0)
    headerDividerTop:SetPoint("TOP", frame, "TOP", 0, -84)
    headerDividerTop:SetHeight(1)
    headerDividerTop:SetTexture("Interface/Buttons/WHITE8x8")
    headerDividerTop:SetVertexColor(ELV_BORDER[1], ELV_BORDER[2], ELV_BORDER[3], 0.8)

    local headerDividerBottom = frame:CreateTexture(nil, "BORDER", nil, -1)
    headerDividerBottom:SetPoint("LEFT", 0, 0)
    headerDividerBottom:SetPoint("RIGHT", 0, 0)
    headerDividerBottom:SetPoint("TOP", frame, "TOP", 0, -130)
    headerDividerBottom:SetHeight(1)
    headerDividerBottom:SetTexture("Interface/Buttons/WHITE8x8")
    headerDividerBottom:SetVertexColor(0.07, 0.07, 0.07, 0.8)

    local listLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    listLabel:SetPoint("TOPLEFT", 16, -140)
    listLabel:SetText("")
    listLabel:SetTextColor(FONT_TITLE[1], FONT_TITLE[2], FONT_TITLE[3])

    local sortButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    sortButton:SetSize(170, 24)
    sortButton:SetPoint("TOPLEFT", 16, -140)
    styleButton(sortButton)

    local listScroll = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate")
    listScroll:SetPoint("TOPLEFT", 14, -168)
    listScroll:SetPoint("BOTTOMRIGHT", -36, 244)
    styleScrollBar(listScroll)

    local listContent = CreateFrame("Frame", nil, listScroll)
    listContent:SetSize(780, 1)
    listScroll:SetScrollChild(listContent)

    local postPanel = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    postPanel:SetPoint("BOTTOMLEFT", 14, 14)
    postPanel:SetPoint("BOTTOMRIGHT", -14, 14)
    postPanel:SetHeight(216)
    styleBackdrop(postPanel)
    postPanel:SetBackdropColor(ELV_PANEL[1], ELV_PANEL[2], ELV_PANEL[3], ELV_PANEL[4])

    local membersPanel = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    membersPanel:SetPoint("TOPRIGHT", -36, -168)
    membersPanel:SetPoint("BOTTOMRIGHT", -36, 244)
    membersPanel:SetWidth(300)
    styleBackdrop(membersPanel)
    membersPanel:SetBackdropColor(ELV_PANEL[1], ELV_PANEL[2], ELV_PANEL[3], ELV_PANEL[4])

    local membersScroll = CreateFrame("ScrollFrame", nil, membersPanel, "UIPanelScrollFrameTemplate")
    membersScroll:SetPoint("TOPLEFT", 10, -10)
    membersScroll:SetPoint("BOTTOMRIGHT", -28, 10)
    styleScrollBar(membersScroll)

    local membersContent = CreateFrame("Frame", nil, membersScroll)
    membersContent:SetSize(240, 20)
    membersScroll:SetScrollChild(membersContent)

    local membersEmptyText = membersContent:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    membersEmptyText:SetPoint("TOPLEFT", 12, 0)
    membersEmptyText:SetPoint("TOPRIGHT", -4, 0)
    membersEmptyText:SetJustifyH("LEFT")
    membersEmptyText:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])
    membersEmptyText:SetText("Geen actieve clients")

    local postLabel = postPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    postLabel:SetPoint("TOPLEFT", 12, -12)
    postLabel:SetText("")
    postLabel:SetTextColor(FONT_TITLE[1], FONT_TITLE[2], FONT_TITLE[3])

    local titleEdit, discordEdit, textEdit

    titleEdit = CreateFrame("EditBox", nil, postPanel, "InputBoxTemplate")
    titleEdit:SetAutoFocus(false)
    titleEdit:SetSize(300, 26)
    titleEdit:SetPoint("TOPLEFT", 12, -34)
    titleEdit:SetMaxLetters(90)
    titleEdit:SetScript("OnEscapePressed", titleEdit.ClearFocus)
    titleEdit:SetScript("OnEnterPressed", function()
        discordEdit:SetFocus()
    end)
    styleEditBox(titleEdit)

    local titleHint = postPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    titleHint:SetPoint("BOTTOMLEFT", titleEdit, "TOPLEFT", 2, 4)
    titleHint:SetText("Titel")
    titleHint:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])

    discordEdit = CreateFrame("EditBox", nil, postPanel, "InputBoxTemplate")
    discordEdit:SetAutoFocus(false)
    discordEdit:SetPoint("LEFT", titleEdit, "RIGHT", 20, 0)
    discordEdit:SetPoint("RIGHT", -12, 0)
    discordEdit:SetHeight(26)
    discordEdit:SetMaxLetters(220)
    discordEdit:SetScript("OnEscapePressed", discordEdit.ClearFocus)
    discordEdit:SetScript("OnEnterPressed", function()
        textEdit:SetFocus()
    end)
    styleEditBox(discordEdit)

    local discordHint = postPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    discordHint:SetPoint("BOTTOMLEFT", discordEdit, "TOPLEFT", 2, 4)
    discordHint:SetText("Discord link (optioneel)")
    discordHint:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])

    local textLabel = postPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    textLabel:SetPoint("TOPLEFT", 12, -66)
    textLabel:SetText("Tekst")
    textLabel:SetTextColor(FONT_GOLD[1], FONT_GOLD[2], FONT_GOLD[3])

    local textContainer = CreateFrame("Frame", nil, postPanel, "BackdropTemplate")
    textContainer:SetPoint("TOPLEFT", 12, -82)
    textContainer:SetPoint("TOPRIGHT", -32, -82)
    textContainer:SetHeight(88)
    styleBackdrop(textContainer)
    textContainer:SetBackdropColor(0.08, 0.08, 0.08, 0.96)

    local textScroll = CreateFrame("ScrollFrame", nil, textContainer, "UIPanelScrollFrameTemplate")
    textScroll:SetPoint("TOPLEFT", 6, -6)
    textScroll:SetPoint("BOTTOMRIGHT", -24, 6)
    styleScrollBar(textScroll)

    textEdit = CreateFrame("EditBox", nil, textScroll)
    textEdit:SetMultiLine(true)
    textEdit:SetAutoFocus(false)
    textEdit:EnableMouse(true)
    textEdit:EnableKeyboard(true)
    if textEdit.SetPropagateKeyboardInput then
        textEdit:SetPropagateKeyboardInput(false)
    end
    if textEdit.SetAltArrowKeyMode then
        textEdit:SetAltArrowKeyMode(false)
    end
    textEdit:SetFontObject("ChatFontNormal")
    textEdit:SetTextColor(0.95, 0.95, 0.95, 1)
    if textEdit.SetShadowColor then
        textEdit:SetShadowColor(0, 0, 0, 0.7)
        textEdit:SetShadowOffset(1, -1)
    end

    local caretFrame = CreateFrame("Frame", nil, textEdit)
    caretFrame:SetSize(2, 14)
    caretFrame:SetFrameLevel(math.max(textEdit:GetFrameLevel() + 8, 8))
    local caretTexture = caretFrame:CreateTexture(nil, "OVERLAY")
    caretTexture:SetAllPoints()
    caretTexture:SetColorTexture(1, 1, 1, 1)
    caretFrame:Hide()

    local caretBlink = caretFrame:CreateAnimationGroup()
    local caretFadeOut = caretBlink:CreateAnimation("Alpha")
    caretFadeOut:SetOrder(1)
    caretFadeOut:SetFromAlpha(1)
    caretFadeOut:SetToAlpha(0)
    caretFadeOut:SetDuration(0.45)

    local caretFadeIn = caretBlink:CreateAnimation("Alpha")
    caretFadeIn:SetOrder(2)
    caretFadeIn:SetFromAlpha(0)
    caretFadeIn:SetToAlpha(1)
    caretFadeIn:SetDuration(0.45)
    caretBlink:SetLooping("REPEAT")

    local function setCustomCaretVisible(isVisible)
        if isVisible then
            caretFrame:SetAlpha(1)
            caretFrame:Show()
            if not caretBlink:IsPlaying() then
                caretBlink:Play()
            end
        else
            if caretBlink:IsPlaying() then
                caretBlink:Stop()
            end
            caretFrame:Hide()
        end
    end

    local function updateCustomCaretPosition(x, y, h)
        local cursorX = tonumber(x) or 0
        local cursorY = tonumber(y) or 0
        local cursorHeight = math.max(12, tonumber(h) or 14)

        caretFrame:ClearAllPoints()
        caretFrame:SetSize(2, cursorHeight)
        caretFrame:SetPoint("TOPLEFT", textEdit, "TOPLEFT", cursorX + 1, cursorY)
    end

    local function normalizeArrowKey(key)
        local k = tostring(key or ""):upper()
        if k == "UP" or k == "ARROWUP" then
            return "UP"
        elseif k == "DOWN" or k == "ARROWDOWN" then
            return "DOWN"
        elseif k == "LEFT" or k == "ARROWLEFT" then
            return "LEFT"
        elseif k == "RIGHT" or k == "ARROWRIGHT" then
            return "RIGHT"
        end
        return ""
    end

    local function getCursorClamped(editBox)
        local text = editBox:GetText() or ""
        local textLength = #text
        local cursorPos = math.floor(tonumber(editBox:GetCursorPosition()) or 0)

        if cursorPos < 0 then
            cursorPos = 0
        elseif cursorPos > textLength then
            cursorPos = textLength
        end

        return text, textLength, cursorPos
    end

    local function getVerticalCursorTarget(editBox, direction)
        if not editBox or direction == 0 then
            return nil
        end

        local text, textLength, cursorPos = getCursorClamped(editBox)
        local prefix = text:sub(1, cursorPos)
        local currentLineBreak = prefix:match(".*()\n")
        local currentLineStart = currentLineBreak and (currentLineBreak + 1) or 1
        local column = cursorPos - (currentLineStart - 1)

        if direction < 0 then
            if currentLineStart <= 1 then
                return nil
            end

            local previousLineEnd = currentLineStart - 2
            local beforePrevious = text:sub(1, previousLineEnd)
            local previousLineBreak = beforePrevious:match(".*()\n")
            local previousLineStart = previousLineBreak and (previousLineBreak + 1) or 1
            local previousLineLength = previousLineEnd - previousLineStart + 1
            return (previousLineStart - 1) + math.min(column, math.max(0, previousLineLength))
        end

        local currentLineEnd = text:find("\n", currentLineStart, true)
        if not currentLineEnd then
            return nil
        end

        local nextLineStart = currentLineEnd + 1
        local nextLineBreak = text:find("\n", nextLineStart, true)
        local nextLineEnd = nextLineBreak and (nextLineBreak - 1) or textLength
        local nextLineLength = nextLineEnd - nextLineStart + 1
        return (nextLineStart - 1) + math.min(column, math.max(0, nextLineLength))
    end

    textEdit:SetWidth(756)
    textEdit:SetHeight(88)
    textEdit:SetMaxLetters(1000)
    textEdit:SetScript("OnEscapePressed", textEdit.ClearFocus)
    textEdit:SetScript("OnMouseDown", function(self)
        self:SetFocus()
        setCustomCaretVisible(true)
    end)
    textEdit:HookScript("OnEditFocusGained", function(self)
        self:EnableKeyboard(true)
        if self.SetPropagateKeyboardInput then
            self:SetPropagateKeyboardInput(false)
        end
        if self.SetAltArrowKeyMode then
            self:SetAltArrowKeyMode(false)
        end
        self:SetAlpha(1)
        if self.GetCursorPosition and self.SetCursorPosition then
            local pos = tonumber(self:GetCursorPosition()) or 0
            self:SetCursorPosition(math.max(0, pos))
        end
        setCustomCaretVisible(true)
    end)
    textEdit:HookScript("OnKeyDown", function(self, key)
        local arrowKey = normalizeArrowKey(key)
        if arrowKey == "" then
            return
        end

        if arrowKey == "UP" or arrowKey == "DOWN" then
            local target = getVerticalCursorTarget(self, arrowKey == "UP" and -1 or 1)
            if target ~= nil and C_Timer and C_Timer.After then
                C_Timer.After(0, function()
                    if self and self:HasFocus() then
                        self:SetCursorPosition(target)
                    end
                end)
            end
        end
    end)
    textEdit:HookScript("OnEditFocusLost", function()
        setCustomCaretVisible(false)
    end)
    textScroll:SetScrollChild(textEdit)

    local textMeasure = textContainer:CreateFontString(nil, "ARTWORK", "ChatFontNormal")
    textMeasure:Hide()
    textMeasure:SetJustifyH("LEFT")
    textMeasure:SetJustifyV("TOP")
    textMeasure:SetWordWrap(true)

    local function syncTextEditHeight()
        local minHeight = math.max(88, textScroll:GetHeight() or 88)
        local maxHeight = math.max(minHeight, 260)
        textMeasure:SetWidth(math.max(220, textEdit:GetWidth() or 220))
        textMeasure:SetText(textEdit:GetText() or "")
        local contentHeight = math.max(minHeight, (textMeasure:GetStringHeight() or 0) + 18)
        contentHeight = math.min(contentHeight, maxHeight)
        textEdit:SetHeight(contentHeight)
    end

    local function keepCursorVisible(self, x, y, _, h)
        local cursorTop = math.max(0, -(tonumber(y) or 0))
        local cursorHeight = math.max(12, tonumber(h) or 0)

        local scrollTop = textScroll:GetVerticalScroll() or 0
        local viewHeight = textScroll:GetHeight() or 0
        local viewBottom = scrollTop + viewHeight
        local cursorBottom = cursorTop + cursorHeight

        if cursorTop < scrollTop then
            textScroll:SetVerticalScroll(cursorTop)
        elseif cursorBottom > viewBottom then
            textScroll:SetVerticalScroll(math.max(0, cursorBottom - viewHeight))
        end

        updateCustomCaretPosition(x, y, h)
        setCustomCaretVisible(self and self:HasFocus())
    end

    textEdit:SetScript("OnCursorChanged", keepCursorVisible)

    textContainer:SetScript("OnMouseDown", function()
        textEdit:SetFocus()
        setCustomCaretVisible(true)
    end)

    attachTabNavigation({ titleEdit, discordEdit, textEdit })

    local function persistMainFrameLayout()
        if not NS.UpdateConfig then
            return
        end

        NS.UpdateConfig({
            mainFrameWidth = frame:GetWidth() or 860,
            mainFrameHeight = frame:GetHeight() or 620,
            mainFrameScale = frame:GetScale() or 1,
        })
    end

    local resizeGrip = CreateFrame("Button", nil, frame)
    resizeGrip:SetSize(18, 18)
    resizeGrip:SetPoint("BOTTOMRIGHT", -7, 7)
    resizeGrip:SetNormalTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
    resizeGrip:SetHighlightTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Highlight")
    resizeGrip:SetPushedTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Down")
    resizeGrip:SetScript("OnMouseDown", function(_, button)
        if button == "LeftButton" then
            frame:StartSizing("BOTTOMRIGHT")
        end
    end)
    resizeGrip:SetScript("OnMouseUp", function()
        frame:StopMovingOrSizing()
        persistMainFrameLayout()
    end)

    local function updateResponsiveLayout()
        local listWidth = math.max(300, listScroll:GetWidth() - 24)
        listContent:SetWidth(listWidth)

        local textWidth = math.max(220, textScroll:GetWidth())
        textEdit:SetWidth(textWidth)
        syncTextEditHeight()

        if ui.frame and ui.frame:IsShown() then
            refreshRows()
        end
    end

    local function persistMainLayoutFromRestoreState()
        if not NS.UpdateConfig then
            return
        end

        local restore = ui.mainRestoreLayout
        if restore then
            NS.UpdateConfig({
                mainFrameWidth = restore.width or 860,
                mainFrameHeight = restore.height or 620,
                mainFrameScale = restore.scale or 1,
            })
        end
    end

    local function updateMaximizeButtonLabel()
        if ui.mainIsMaximized then
            maximizeBtn:SetText("❐")
        else
            maximizeBtn:SetText("□")
        end
    end

    local function setFrameMaximized(shouldMaximize)
        if shouldMaximize == ui.mainIsMaximized then
            return
        end

        if shouldMaximize then
            local point, relativeTo, relativePoint, xOfs, yOfs = frame:GetPoint(1)
            ui.mainRestoreLayout = {
                point = point or "CENTER",
                relativeTo = relativeTo,
                relativePoint = relativePoint or point or "CENTER",
                xOfs = tonumber(xOfs) or 0,
                yOfs = tonumber(yOfs) or 0,
                width = frame:GetWidth() or 860,
                height = frame:GetHeight() or 620,
                scale = frame:GetScale() or 1,
            }

            frame:StopMovingOrSizing()
            frame:ClearAllPoints()
            frame:SetScale(1)
            frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 6, -6)
            frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -6, 6)
            frame:SetMovable(false)
            frame:SetResizable(false)
            if resizeGrip then
                resizeGrip:Hide()
            end
            ui.mainIsMaximized = true
        else
            local restore = ui.mainRestoreLayout
            frame:StopMovingOrSizing()
            frame:ClearAllPoints()
            if restore then
                frame:SetScale(restore.scale or 1)
                frame:SetSize(restore.width or 860, restore.height or 620)
                frame:SetPoint(
                    restore.point or "CENTER",
                    restore.relativeTo or UIParent,
                    restore.relativePoint or "CENTER",
                    restore.xOfs or 0,
                    restore.yOfs or 0
                )
            else
                frame:SetScale(frameScale)
                frame:SetSize(frameWidth, frameHeight)
                frame:SetPoint("CENTER")
            end
            frame:SetMovable(true)
            frame:SetResizable(true)
            if resizeGrip then
                resizeGrip:Show()
            end
            ui.mainIsMaximized = false
        end

        updateMaximizeButtonLabel()
        updateResponsiveLayout()
    end

    maximizeBtn:SetScript("OnClick", function()
        setFrameMaximized(not ui.mainIsMaximized)
    end)
    updateMaximizeButtonLabel()

    frame:SetScript("OnSizeChanged", function()
        updateResponsiveLayout()
    end)

    local counter = postPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    counter:SetPoint("BOTTOMLEFT", 12, 12)
    counter:SetText("0/1000")
    counter:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])

    local function updateCounter()
        local length = #(textEdit:GetText() or "")
        if length > 1000 then
            counter:SetTextColor(1, 0.25, 0.25)
        else
            counter:SetTextColor(0.8, 0.8, 0.8)
        end
        counter:SetText(string.format("%d/1000", length))
    end

    textEdit:SetScript("OnTextChanged", function()
        syncTextEditHeight()
        updateCounter()
    end)

    local postButton = CreateFrame("Button", nil, postPanel, "UIPanelButtonTemplate")
    postButton:SetSize(120, 26)
    postButton:SetPoint("BOTTOMRIGHT", -12, 10)
    postButton:SetText("Plaatsen")
    styleButton(postButton)

    local permText = postPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    permText:SetPoint("RIGHT", postButton, "LEFT", -12, 0)

    postButton:SetScript("OnClick", function()
        local ok, err
        if ui.editingPostId and NS.UpdateLocalPost then
            ok, err = NS.UpdateLocalPost(ui.editingPostId, titleEdit:GetText(), textEdit:GetText(), discordEdit:GetText(), false)
        else
            ok, err = NS.CreateLocalPost(titleEdit:GetText(), textEdit:GetText(), discordEdit:GetText(), false)
        end

        if ok then
            titleEdit:SetText("")
            textEdit:SetText("")
            discordEdit:SetText("")
            ui.editingPostId = nil
            postButton:SetText("Plaatsen")
            updateCounter()
            return
        end

        if err == "TITLE_REQUIRED" then
            UIErrorsFrame:AddMessage("Titel is verplicht.", 1, 0.2, 0.2)
        elseif err == "TEXT_REQUIRED" then
            UIErrorsFrame:AddMessage("Tekst is verplicht.", 1, 0.2, 0.2)
        elseif err == "TEXT_TOO_LONG" then
            UIErrorsFrame:AddMessage("Tekst mag maximaal 1000 karakters zijn.", 1, 0.2, 0.2)
        elseif err == "DISCORD_LINK_INVALID" then
            UIErrorsFrame:AddMessage("Gebruik een geldige Discord link (https://discord.gg/...)", 1, 0.2, 0.2)
        elseif err == "NO_PERMISSION" then
            UIErrorsFrame:AddMessage("Jouw guildrang heeft geen postrechten.", 1, 0.2, 0.2)
        elseif err == "NOT_FOUND" then
            UIErrorsFrame:AddMessage("Dit bericht bestaat niet meer.", 1, 0.2, 0.2)
            ui.editingPostId = nil
            postButton:SetText("Plaatsen")
        end
    end)

    textEdit:SetScript("OnEnterPressed", function(self)
        if IsShiftKeyDown() then
            self:Insert("\n")
            return
        end

        postButton:Click()
    end)

    local function updateSortButtonLabel()
        if ui.sortNewestFirst == false then
            sortButton:SetText("Oud -> Nieuw")
        else
            sortButton:SetText("Nieuw -> Oud")
        end
    end

    local cfg = NS.GetConfig and NS.GetConfig() or { maxPosts = 0, pruneDays = 0, showLoginMessage = true, sortNewestFirst = true }
    ui.sortNewestFirst = cfg.sortNewestFirst ~= false
    updateSortButtonLabel()
    sortButton:SetScript("OnClick", function()
        ui.sortNewestFirst = not ui.sortNewestFirst
        if NS.UpdateConfig then
            local currentCfg = NS.GetConfig and NS.GetConfig() or cfg
            NS.UpdateConfig({
                maxPosts = currentCfg.maxPosts,
                pruneDays = currentCfg.pruneDays,
                showLoginMessage = currentCfg.showLoginMessage,
                sortNewestFirst = ui.sortNewestFirst,
            })
            cfg = NS.GetConfig and NS.GetConfig() or cfg
        end
        updateSortButtonLabel()
        refreshRows()
    end)

    local function applySearchFromInput()
        ui.activeSearchQuery = normalizeSearchQuery(searchEdit:GetText() or "")
        refreshRows()
    end

    searchButton:SetScript("OnClick", function()
        applySearchFromInput()
    end)
    searchEdit:SetScript("OnEnterPressed", function()
        applySearchFromInput()
    end)
    resetFilterButton:SetScript("OnClick", function()
        ui.activeSearchQuery = ""
        searchEdit:SetText("")
        refreshRows()
    end)

    ui.frame = frame
    ui.titleEdit = titleEdit
    ui.textEdit = textEdit
    ui.discordEdit = discordEdit
    ui.counter = counter
    ui.updateCounter = updateCounter
    ui.permText = permText
    ui.postButton = postButton
    ui.listContent = listContent
    ui.listScroll = listScroll
    ui.sortButton = sortButton
    ui.searchButton = searchButton
    ui.searchEdit = searchEdit
    ui.resetFilterButton = resetFilterButton
    ui.maximizeButton = maximizeBtn
    ui.postPanel = postPanel
    ui.mainCommStatus = mainCommStatus
    ui.mainMembersPanel = membersPanel
    ui.mainMembersContent = membersContent
    ui.mainMembersEmptyText = membersEmptyText
    ui.mainMembersRows = {}

    frame:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
    end)

    local originalOnHide = frame:GetScript("OnHide")
    frame:SetScript("OnHide", function(self)
        if originalOnHide then
            originalOnHide(self)
        end
        if ui.mainIsMaximized and ui.mainRestoreLayout then
            persistMainLayoutFromRestoreState()
        end
    end)

    syncTextEditHeight()
    updateCounter()
    refreshCommunicationStatusTexts()
    refreshCommunicationMembersPanel()
    updateResponsiveLayout()
    return frame
end

local function configurePermissionState()
    local canManage = NS.CanManagePosts and NS.CanManagePosts() or false

    if ui.postPanel and ui.listScroll then
        local canViewMembers = NS.CanViewCommunicationMembers and NS.CanViewCommunicationMembers() or false
        local rightInset = canViewMembers and 346 or 36

        ui.listScroll:ClearAllPoints()
        ui.listScroll:SetPoint("TOPLEFT", 14, -168)

        if canManage then
            ui.postPanel:Show()
            ui.listScroll:SetPoint("BOTTOMRIGHT", -rightInset, 244)
        else
            ui.postPanel:Hide()
            ui.listScroll:SetPoint("BOTTOMRIGHT", -rightInset, 18)
        end

        if ui.mainMembersPanel then
            ui.mainMembersPanel:ClearAllPoints()
            ui.mainMembersPanel:SetPoint("TOPRIGHT", -36, -168)
            if canManage then
                ui.mainMembersPanel:SetPoint("BOTTOMRIGHT", -36, 244)
            else
                ui.mainMembersPanel:SetPoint("BOTTOMRIGHT", -36, 18)
            end
        end
    end

    if canManage then
        ui.permText:SetText("Je kunt posten. Wijzigen/verwijderen: guild master of auteur")
        ui.permText:SetTextColor(0.45, 0.9, 0.45)
        ui.postButton:Enable()
    else
        ui.permText:SetText("Posten: volgens ingestelde guildrangen. Wijzigen/verwijderen: guild master of auteur")
        ui.permText:SetTextColor(1, 0.35, 0.35)
        ui.postButton:Disable()
    end
end

local function releaseRows()
    for _, row in ipairs(ui.rows) do
        row:Hide()
    end
end

local function formatLikeDisplayName(name)
    local raw = tostring(name or "")
    if raw == "" then
        return ""
    end

    local function capitalizeToken(token)
        return (token:gsub("^%l", string.upper))
    end

    local base, realm = raw:match("^([^%-]+)%-(.+)$")
    if base and realm then
        return capitalizeToken(base) .. "-" .. capitalizeToken(realm)
    end

    return capitalizeToken(raw)
end

local function hexToRgb(hex)
    local clean = tostring(hex or "")
    if #clean ~= 6 then
        return FONT_META[1], FONT_META[2], FONT_META[3]
    end

    local r = tonumber(clean:sub(1, 2), 16) or 255
    local g = tonumber(clean:sub(3, 4), 16) or 255
    local b = tonumber(clean:sub(5, 6), 16) or 255
    return r / 255, g / 255, b / 255
end

local function showLikesTooltip(owner, post)
    if not owner or not post then
        return
    end

    local likeNames = NS.GetPostLikeNames and NS.GetPostLikeNames(post) or {}

    GameTooltip:SetOwner(owner, "ANCHOR_TOPLEFT")
    GameTooltip:ClearLines()
    GameTooltip:AddLine(string.format("Likes (%d)", #likeNames), 0.94, 0.78, 0.32)

    if #likeNames == 0 then
        GameTooltip:AddLine("Nog geen likes", FONT_META[1], FONT_META[2], FONT_META[3])
    else
        for i = 1, #likeNames do
            local rawName = likeNames[i]
            local displayName = formatLikeDisplayName(rawName)
            local classWrapped = NS.GetClassColoredMemberName and NS.GetClassColoredMemberName(rawName) or rawName
            local classHex = tostring(classWrapped):match("|cff(%x%x%x%x%x%x)")
            local r, g, b = hexToRgb(classHex)
            GameTooltip:AddLine(displayName, r, g, b)
        end
    end

    GameTooltip:Show()
end

local function showReadByTooltip(owner, post)
    if not owner or not post then
        return
    end

    local readerNames = NS.GetPostReaderNames and NS.GetPostReaderNames(post) or {}

    GameTooltip:SetOwner(owner, "ANCHOR_TOPLEFT")
    GameTooltip:ClearLines()
    GameTooltip:AddLine(string.format("Gelezen door (%d)", #readerNames), 0.94, 0.78, 0.32)

    if #readerNames == 0 then
        GameTooltip:AddLine("Nog niet gelezen", FONT_META[1], FONT_META[2], FONT_META[3])
    else
        for i = 1, #readerNames do
            local rawName = readerNames[i]
            local displayName = formatLikeDisplayName(rawName)
            local classWrapped = NS.GetClassColoredMemberName and NS.GetClassColoredMemberName(rawName) or rawName
            local classHex = tostring(classWrapped):match("|cff(%x%x%x%x%x%x)")
            local r, g, b = hexToRgb(classHex)
            GameTooltip:AddLine(displayName, r, g, b)
        end
    end

    GameTooltip:Show()
end

local function acquireRow(index)
    local row = ui.rows[index]
    if row then
        row:Show()
        return row
    end

    row = CreateFrame("Frame", nil, ui.listContent, "BackdropTemplate")
    styleBackdrop(row)
    row:SetBackdropColor(ELV_PANEL[1], ELV_PANEL[2], ELV_PANEL[3], ELV_PANEL[4])

    row.glow = row:CreateTexture(nil, "BACKGROUND", nil, -1)
    row.glow:SetPoint("TOPLEFT", 2, -2)
    row.glow:SetPoint("TOPRIGHT", -2, -2)
    row.glow:SetHeight(1)
    row.glow:SetTexture("Interface/Buttons/WHITE8x8")
    row.glow:SetVertexColor(ELV_ACCENT[1], ELV_ACCENT[2], ELV_ACCENT[3], 0.25)

    row.title = row:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    row.title:SetPoint("TOPLEFT", 12, -10)
    row.title:SetPoint("TOPRIGHT", -12, -10)
    row.title:SetJustifyH("LEFT")

    row.meta = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    row.meta:SetPoint("TOPLEFT", row.title, "BOTTOMLEFT", 0, -6)
    row.meta:SetPoint("TOPRIGHT", row.title, "BOTTOMRIGHT", 0, -6)
    row.meta:SetJustifyH("LEFT")
    row.meta:SetTextColor(0.95, 0.78, 0.32)

    row.body = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    row.body:SetPoint("TOPLEFT", row.meta, "BOTTOMLEFT", 0, -8)
    row.body:SetWidth(730)
    row.body:SetJustifyH("LEFT")
    row.body:SetJustifyV("TOP")
    row.body:SetTextColor(FONT_LABEL[1], FONT_LABEL[2], FONT_LABEL[3])
    applyRowFonts(row)

    row.readCount = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    row.readCount:SetPoint("BOTTOMRIGHT", -12, 10)
    row.readCount:SetJustifyH("RIGHT")
    row.readCount:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])

    row.likeDetails = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    row.likeDetails:SetPoint("BOTTOMLEFT", 12, 10)
    row.likeDetails:SetPoint("BOTTOMRIGHT", row.readCount, "BOTTOMLEFT", -10, 0)
    row.likeDetails:SetJustifyH("LEFT")
    row.likeDetails:SetWordWrap(false)
    row.likeDetails:SetTextColor(FONT_META[1], FONT_META[2], FONT_META[3])
    row.likeDetails:EnableMouse(true)

    row.likeBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
    row.likeBtn:SetSize(96, 22)
    row.likeBtn:SetText("Vind ik leuk")
    styleButton(row.likeBtn)

    row.reactBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
    row.reactBtn:SetSize(88, 22)
    row.reactBtn:SetText("Reageren")
    styleButton(row.reactBtn)

    -- Comment input area (shown when react button is clicked)
    row.commentBox = CreateFrame("Frame", nil, row, "BackdropTemplate")
    styleBackdrop(row.commentBox)
    row.commentBox:SetBackdropColor(ELV_BG[1], ELV_BG[2], ELV_BG[3], ELV_BG[4])
    row.commentBox:SetHeight(28)

    row.commentInput = CreateFrame("EditBox", nil, row.commentBox, "InputBoxTemplate")
    row.commentInput:SetPoint("LEFT", row.commentBox, "LEFT", 6, 0)
    row.commentInput:SetPoint("RIGHT", row.commentBox, "RIGHT", -100, 0)
    row.commentInput:SetHeight(20)
    row.commentInput:SetPoint("TOP", row.commentBox, "TOP", 0, -4)
    row.commentInput:SetAutoFocus(false)
    row.commentInput:SetMaxLetters(200)
    row.commentInput:SetTextColor(FONT_LABEL[1], FONT_LABEL[2], FONT_LABEL[3])

    row.commentSubmit = CreateFrame("Button", nil, row.commentBox, "UIPanelButtonTemplate")
    row.commentSubmit:SetSize(88, 22)
    row.commentSubmit:SetText("Plaatsen")
    row.commentSubmit:SetPoint("RIGHT", row.commentBox, "RIGHT", -4, 0)
    row.commentSubmit:SetPoint("TOP", row.commentBox, "TOP", 0, -3)
    styleButton(row.commentSubmit)

    -- Toggle button to expand/collapse comments
    row.commentToggle = CreateFrame("Button", nil, row)
    row.commentToggle:SetHeight(22)
    row.commentToggle.label = row.commentToggle:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    row.commentToggle.label:SetAllPoints()
    row.commentToggle.label:SetJustifyH("LEFT")
    row.commentToggle.label:SetTextColor(ELV_ACCENT[1], ELV_ACCENT[2], ELV_ACCENT[3])

    -- Pool of reusable comment entry frames
    row.commentPool = {}

    row.copyDiscord = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
    row.copyDiscord:SetSize(160, 22)
    row.copyDiscord:SetText("Kopieer Discord link")
    styleButton(row.copyDiscord)

    row.deleteBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
    row.deleteBtn:SetSize(120, 22)
    row.deleteBtn:SetText("Verwijderen")
    styleButton(row.deleteBtn)

    row.editBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
    row.editBtn:SetSize(120, 22)
    row.editBtn:SetText("Wijzigen")
    styleButton(row.editBtn)

    ui.rows[index] = row
    return row
end

refreshRows = function()
    if not ui.frame then
        return
    end

    configurePermissionState()
    refreshCommunicationMembersPanel()

    local listWidth = math.max(
        300,
        ((ui.listScroll and ui.listScroll:GetWidth()) or ui.listContent:GetWidth() or 324) - 24
    )
    ui.listContent:SetWidth(listWidth)
    if ui.listScroll and ui.listScroll.UpdateScrollChildRect then
        ui.listScroll:UpdateScrollChildRect()
    end

    releaseRows()

    local y = -2
    local posts = NS.GetPosts and NS.GetPosts() or {}
    local displayPosts = getDisplayPosts(posts)
    local activeSearch = normalizeSearchQuery(ui.activeSearchQuery)
    if activeSearch ~= "" then
        local filteredPosts = {}
        for i = 1, #displayPosts do
            local post = displayPosts[i]
            if postMatchesSearch(post, activeSearch) then
                table.insert(filteredPosts, post)
            end
        end
        displayPosts = filteredPosts
    end
    local visiblePostIds = {}

    for index, post in ipairs(displayPosts) do
        local row = acquireRow(index)
        applyRowFonts(row)
        table.insert(visiblePostIds, post.id)

        row:SetPoint("TOPLEFT", 4, y)
        row:SetWidth(790)

        row.title:SetText(post.title or "(zonder titel)")
        row.meta:SetText(string.format("Door %s | Aangemaakt: %s", post.author or "Onbekend", NS.FormatTimestamp(post.createdAt or time())))
        row.body:SetText(post.text or "")

        local rowWidth = math.max(300, listWidth - 10)
        row:SetWidth(rowWidth)
        row.body:SetWidth(math.max(220, rowWidth - 60))

        -- Gather threaded comments and compute dynamic layout/height.
        local comments = NS.GetPostComments and NS.GetPostComments(post) or {}
        local displayComments = flattenThreadedComments(comments)
        local commentCount = #comments
        local inputOpen = commentInputOpen[post.id]
        local expanded = commentExpanded[post.id]
        local COMMENT_ENTRY_META_H = 16
        local COMMENT_BUTTONS_H = 22
        local COMMENT_INPUT_H = 34
        local COMMENT_TOGGLE_H = 26
        local COMMENT_SECTION_PAD = 6
        local COMMENT_ENTRY_GAP = 4
        local COMMENT_BASE_INDENT = 10
        local COMMENT_INDENT_STEP = 16

        if not commentEditorState[post.id] then
            commentEditorState[post.id] = {}
        end
        local postEditorState = commentEditorState[post.id]

        local entryHeights = {}
        local entryTextWidths = {}
        local entryIndents = {}

        if expanded and #displayComments > 0 then
            for i, c in ipairs(displayComments) do
                local depth = math.max(1, tonumber(c.depth) or 1)
                local indent = COMMENT_BASE_INDENT + ((depth - 1) * COMMENT_INDENT_STEP)
                local entryWidth = math.max(180, rowWidth - 30 - indent)
                local textWidth = math.max(150, entryWidth - 20)

                local entry = acquireCommentEntry(row, i)
                entry:SetWidth(entryWidth)
                entry.textFS:SetWidth(textWidth)
                entry.textFS:SetText(c.text or "")

                local textHeight = math.max(14, entry.textFS:GetStringHeight())
                local editor = postEditorState[c.id]
                local editorExtra = editor and (COMMENT_INPUT_H + COMMENT_SECTION_PAD) or 0
                local entryHeight = COMMENT_ENTRY_META_H + textHeight + COMMENT_BUTTONS_H + 12 + editorExtra

                entryHeights[i] = entryHeight
                entryTextWidths[i] = textWidth
                entryIndents[i] = indent
            end
        end

        if row.commentPool then
            for i = (#displayComments + 1), #row.commentPool do
                if row.commentPool[i] then
                    row.commentPool[i]:Hide()
                end
            end
            if not expanded then
                for i = 1, #displayComments do
                    if row.commentPool[i] then
                        row.commentPool[i]:Hide()
                    end
                end
            end
        end

        local commentExtraHeight = 0
        if inputOpen then
            commentExtraHeight = commentExtraHeight + COMMENT_INPUT_H + COMMENT_SECTION_PAD
        end
        if commentCount > 0 then
            commentExtraHeight = commentExtraHeight + COMMENT_TOGGLE_H
            if expanded then
                for i = 1, #displayComments do
                    commentExtraHeight = commentExtraHeight + (entryHeights[i] or 52) + COMMENT_ENTRY_GAP
                end
                commentExtraHeight = commentExtraHeight + COMMENT_SECTION_PAD
            end
        end

        local textHeight = row.body:GetStringHeight()
        local bodyToButtonsSpacing = 12
        local bottomPadding = 8
        local uniqueReadCount = NS.GetUniqueReadCount and NS.GetUniqueReadCount(post) or 0
        local likeCount = NS.GetPostLikeCount and NS.GetPostLikeCount(post) or 0
        row.readCount:SetText(string.format("Gelezen: %d | Likes: %d", uniqueReadCount, likeCount))
        row.readCount:SetScript("OnEnter", function(self)
            showReadByTooltip(self, post)
        end)
        row.readCount:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)

        row.likeDetails:SetText("")
        row.likeDetails:SetScript("OnEnter", nil)
        row.likeDetails:SetScript("OnLeave", nil)
        row.likeDetails:Hide()

        local likedByLocal = NS.HasLocalPlayerLikedPost and NS.HasLocalPlayerLikedPost(post) or false
        row.likeBtn:SetAlpha(likedByLocal and 1 or 0.7)
        row.likeBtn:Show()
        row.likeBtn:ClearAllPoints()
        row.likeBtn:SetPoint("TOPLEFT", row.body, "BOTTOMLEFT", 0, -bodyToButtonsSpacing)
        row.likeBtn:SetScript("OnEnter", function(self)
            showLikesTooltip(self, post)
        end)
        row.likeBtn:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)
        row.likeBtn:SetScript("OnClick", function()
            if NS.TogglePostLike then
                NS.TogglePostLike(post.id)
            end
        end)

        -- Reageren button
        row.reactBtn:Show()
        row.reactBtn:ClearAllPoints()
        row.reactBtn:SetPoint("LEFT", row.likeBtn, "RIGHT", 8, 0)
        row.reactBtn:SetScript("OnClick", function()
            commentInputOpen[post.id] = not commentInputOpen[post.id]
            if commentInputOpen[post.id] then
                row.commentInput:SetText("")
            end
            refreshRows()
        end)

        local discordLink = post.discordLink or ""
        local hasDiscordLink = discordLink ~= ""
        if hasDiscordLink then
            row.copyDiscord:Show()
            row.copyDiscord:SetScript("OnClick", function()
                showCopyDialog(discordLink)
            end)
        else
            row.copyDiscord:Hide()
        end

        local canEditThisPost = NS.CanDeletePost and NS.CanDeletePost(post.id) or false
        if canEditThisPost then
            row.editBtn:Show()
            row.editBtn:SetScript("OnClick", function()
                ui.editingPostId = post.id
                if ui.titleEdit then
                    ui.titleEdit:SetText(post.title or "")
                end
                if ui.textEdit then
                    ui.textEdit:SetText(post.text or "")
                end
                if ui.discordEdit then
                    ui.discordEdit:SetText(post.discordLink or "")
                end
                if ui.postButton then
                    ui.postButton:SetText("Wijziging opslaan")
                end
                if ui.updateCounter then
                    ui.updateCounter()
                end
                if ui.textEdit then
                    ui.textEdit:SetFocus()
                end
            end)

            row.deleteBtn:Show()
            row.deleteBtn:SetScript("OnClick", function()
                local ok = NS.DeleteLocalPost(post.id)
                if not ok then
                    UIErrorsFrame:AddMessage("Verwijderen mislukt.", 1, 0.2, 0.2)
                end
            end)
        else
            row.editBtn:Hide()
            row.deleteBtn:Hide()
        end

        local actionButtons = { row.likeBtn, row.reactBtn }
        if hasDiscordLink then
            table.insert(actionButtons, row.copyDiscord)
        end
        if canEditThisPost then
            table.insert(actionButtons, row.editBtn)
            table.insert(actionButtons, row.deleteBtn)
        end

        local actionGap = 8
        local actionLineHeight = 22
        local actionLineSpacing = 4
        local readReserve = math.max(180, math.ceil((row.readCount:GetStringWidth() or 180) + 28))
        local actionMaxWidth = math.max(240, rowWidth - 40 - readReserve)
        local actionCursorX = 0
        local actionCursorY = 0
        local actionLines = 1

        for i = 1, #actionButtons do
            local btn = actionButtons[i]
            local buttonWidth = btn:GetWidth() or 0
            if actionCursorX > 0 and (actionCursorX + buttonWidth) > actionMaxWidth then
                actionLines = actionLines + 1
                actionCursorX = 0
                actionCursorY = actionCursorY - (actionLineHeight + actionLineSpacing)
            end

            btn:ClearAllPoints()
            btn:SetPoint("TOPLEFT", row.body, "BOTTOMLEFT", actionCursorX, -bodyToButtonsSpacing + actionCursorY)
            actionCursorX = actionCursorX + buttonWidth + actionGap
        end

        local actionExtraHeight = (actionLines - 1) * (actionLineHeight + actionLineSpacing)
        local rowHeight = math.max(118, textHeight + 94 + actionExtraHeight + commentExtraHeight)
        row:SetHeight(rowHeight)

        -- Keep read/likes status visible; reserve space from the action layout width.
        row.readCount:ClearAllPoints()
        row.readCount:SetPoint("TOPRIGHT", row.body, "BOTTOMRIGHT", 0, -bodyToButtonsSpacing)

        row.likeDetails:ClearAllPoints()
        row.likeDetails:SetPoint("TOPLEFT", row.body, "BOTTOMLEFT", 0, -bodyToButtonsSpacing)
        row.likeDetails:SetPoint("RIGHT", row.readCount, "LEFT", -12, 0)

        -- Comment input area (reply to post itself)
        local commentSectionAnchor = row.likeBtn
        if actionLines > 1 then
            commentSectionAnchor = actionButtons[#actionButtons]
        end
        if inputOpen then
            row.commentBox:Show()
            row.commentBox:ClearAllPoints()
            row.commentBox:SetPoint("TOPLEFT", commentSectionAnchor, "BOTTOMLEFT", 0, -COMMENT_SECTION_PAD)
            row.commentBox:SetPoint("TOPRIGHT", row, "TOPRIGHT", -12, 0)
            row.commentSubmit:SetScript("OnClick", function()
                local text = row.commentInput:GetText()
                if NS.AddComment then
                    local ok, err = NS.AddComment(post.id, text, "")
                    if ok then
                        row.commentInput:SetText("")
                        commentInputOpen[post.id] = nil
                        commentExpanded[post.id] = true
                        refreshRows()
                    elseif err == "TEXT_TOO_LONG" then
                        UIErrorsFrame:AddMessage("Reactie mag maximaal 200 tekens bevatten.", 1, 0.4, 0.2)
                    elseif err == "TEXT_REQUIRED" then
                        UIErrorsFrame:AddMessage("Voer een reactie in.", 1, 0.4, 0.2)
                    else
                        UIErrorsFrame:AddMessage("Reactie plaatsen mislukt.", 1, 0.2, 0.2)
                    end
                end
            end)
            row.commentInput:SetScript("OnEnterPressed", function()
                local submit = row.commentSubmit:GetScript("OnClick")
                if submit then
                    submit()
                end
            end)
            commentSectionAnchor = row.commentBox
        else
            row.commentBox:Hide()
        end

        if commentCount > 0 then
            local label = expanded
                and string.format("▼  %d reactie%s", commentCount, commentCount == 1 and "" or "s")
                or string.format("▶  %d reactie%s", commentCount, commentCount == 1 and "" or "s")
            row.commentToggle.label:SetText(label)
            row.commentToggle:Show()
            row.commentToggle:ClearAllPoints()
            row.commentToggle:SetPoint("TOPLEFT", commentSectionAnchor, "BOTTOMLEFT", 0, -COMMENT_SECTION_PAD)
            row.commentToggle:SetPoint("TOPRIGHT", row, "TOPRIGHT", -12, 0)
            row.commentToggle:SetScript("OnClick", function()
                commentExpanded[post.id] = not commentExpanded[post.id]
                refreshRows()
            end)

            if expanded then
                local entryAnchor = row.commentToggle
                local previousIndent = 0
                local canDeleteComments = NS.CanDeletePost and NS.CanDeletePost(post.id) or false

                for i, c in ipairs(displayComments) do
                    local entry = acquireCommentEntry(row, i)
                    local classWrapped = NS.GetClassColoredMemberName and NS.GetClassColoredMemberName(c.author) or c.author
                    local stamp = NS.FormatTimestamp and NS.FormatTimestamp(c.timestamp or 0) or tostring(c.timestamp or "")
                    local entryH = entryHeights[i] or 52
                    local indent = entryIndents[i] or COMMENT_BASE_INDENT
                    local textWidth = entryTextWidths[i] or 180

                    entry.metaFS:SetText(string.format("%s  |  %s", classWrapped, stamp))
                    entry.textFS:SetWidth(textWidth)
                    entry.textFS:SetText(c.text or "")

                    entry:ClearAllPoints()
                    if entryAnchor == row.commentToggle then
                        entry:SetPoint("TOPLEFT", entryAnchor, "BOTTOMLEFT", indent, -COMMENT_ENTRY_GAP)
                    else
                        entry:SetPoint("TOPLEFT", entryAnchor, "BOTTOMLEFT", indent - previousIndent, -COMMENT_ENTRY_GAP)
                    end
                    entry:SetWidth(math.max(180, rowWidth - 30 - indent))
                    entry:SetHeight(entryH)
                    entry.bar:SetHeight(entryH)

                    entry.replyBtn:ClearAllPoints()
                    entry.replyBtn:SetPoint("TOPLEFT", entry.textFS, "BOTTOMLEFT", 0, -4)
                    entry.replyBtn:Show()

                    local buttonAnchor = entry.replyBtn
                    local canEditComment = NS.CanEditComment and NS.CanEditComment(c) or false
                    if canEditComment then
                        entry.editBtn:Show()
                        entry.editBtn:ClearAllPoints()
                        entry.editBtn:SetPoint("LEFT", buttonAnchor, "RIGHT", 6, 0)
                        buttonAnchor = entry.editBtn
                    else
                        entry.editBtn:Hide()
                    end

                    if canDeleteComments then
                        entry.deleteBtn:Show()
                        entry.deleteBtn:ClearAllPoints()
                        entry.deleteBtn:SetPoint("LEFT", buttonAnchor, "RIGHT", 6, 0)
                    else
                        entry.deleteBtn:Hide()
                    end

                    entry.replyBtn:SetScript("OnClick", function()
                        postEditorState[c.id] = { mode = "reply", text = "" }
                        commentExpanded[post.id] = true
                        refreshRows()
                    end)

                    entry.editBtn:SetScript("OnClick", function()
                        postEditorState[c.id] = { mode = "edit", text = c.text or "" }
                        commentExpanded[post.id] = true
                        refreshRows()
                    end)

                    entry.deleteBtn:SetScript("OnClick", function()
                        postEditorState[c.id] = nil
                        showDeleteCommentConfirm(post.id, c.id, c.author)
                    end)

                    local editorState = postEditorState[c.id]
                    if editorState then
                        entry.editorBox:Show()
                        entry.editorBox:ClearAllPoints()
                        entry.editorBox:SetPoint("TOPLEFT", entry.replyBtn, "BOTTOMLEFT", 0, -4)
                        entry.editorBox:SetPoint("TOPRIGHT", entry, "TOPRIGHT", -2, 0)
                        if entry.editorInput:GetText() ~= (editorState.text or "") then
                            entry.editorInput:SetText(editorState.text or "")
                        end

                        entry.editorSave:SetScript("OnClick", function()
                            local text = entry.editorInput:GetText()
                            local ok, err
                            if editorState.mode == "edit" and NS.EditComment then
                                ok, err = NS.EditComment(post.id, c.id, text)
                            elseif editorState.mode == "reply" and NS.AddComment then
                                ok, err = NS.AddComment(post.id, text, c.id)
                            end

                            if ok then
                                postEditorState[c.id] = nil
                                commentExpanded[post.id] = true
                                refreshRows()
                            elseif err == "TEXT_TOO_LONG" then
                                UIErrorsFrame:AddMessage("Reactie mag maximaal 200 tekens bevatten.", 1, 0.4, 0.2)
                            elseif err == "TEXT_REQUIRED" then
                                UIErrorsFrame:AddMessage("Voer een reactie in.", 1, 0.4, 0.2)
                            else
                                UIErrorsFrame:AddMessage("Opslaan van reactie mislukt.", 1, 0.2, 0.2)
                            end
                        end)
                        entry.editorCancel:SetScript("OnClick", function()
                            postEditorState[c.id] = nil
                            refreshRows()
                        end)
                        entry.editorInput:SetScript("OnEnterPressed", function()
                            local save = entry.editorSave:GetScript("OnClick")
                            if save then
                                save()
                            end
                        end)
                    else
                        entry.editorBox:Hide()
                    end

                    entryAnchor = entry
                    previousIndent = indent
                end
            end
        else
            row.commentToggle:Hide()
            if row.commentToggle.label then
                row.commentToggle.label:SetText("")
            end
        end

        y = y - rowHeight - 6
    end

    ui.listContent:SetHeight(math.max(1, -y + 8))

    if NS.MarkPostsRead and #visiblePostIds > 0 then
        NS.MarkPostsRead(visiblePostIds)
    end
end

function NS.RefreshUI()
    local posts = NS.GetPosts and NS.GetPosts() or {}
    local topPostId = posts[1] and posts[1].id or nil
    local postCount = #posts

    if ui._hasSeenPostsBefore then
        local previousCount = ui._lastPostCount or 0
        local added = postCount - previousCount
        local topChanged = topPostId and topPostId ~= ui._lastTopPostId

        if added > 0 and topChanged then
            if isMainShown() then
                triggerLogoPulse()
            else
                ui.unseenCount = (ui.unseenCount or 0) + added
            end
        end
    else
        ui.unseenCount = ui.unseenCount or 0
    end

    ui._hasSeenPostsBefore = true
    ui._lastTopPostId = topPostId
    ui._lastPostCount = postCount

    if isMainShown() then
        refreshRows()
    end

    refreshCommunicationStatusTexts()
    refreshCommunicationMembersPanel()

    updateLauncherVisualState()
end

function NS.ToggleMainFrame()
    createMainFrame()
    if ui.frame:IsShown() then
        ui.frame:Hide()
    else
        ui.frame:Show()
        ui.unseenCount = 0
        refreshRows()

        if C_Timer and C_Timer.After then
            C_Timer.After(0, function()
                if ui.frame and ui.frame:IsShown() then
                    refreshRows()
                end
            end)
        end
    end

    updateLauncherVisualState()
end

function NS.ToggleConfigFrame()
    local frame = createConfigFrame()
    refreshConfigFrame()
    if frame:IsShown() then
        frame:Hide()
    else
        frame:Show()
    end
end
