-- ============================================================================
-- Vamoose's Companion - Settings Panel v2
-- Two-column master-detail: left sidebar (companion select) + right content (4 tabs)
-- Overall: 760x580, sidebar 180px, content 580px
-- ============================================================================

VC = VC or {}
VC.Settings = {}

local WHITE = "Interface\\Buttons\\WHITE8x8"

-- ============================================================================
-- LOCAL HELPERS
-- ============================================================================

local function MakePage(self, parent, pageH, key)
    local TB = VC.Constants.SETTINGS.TAB_BAR_H
    local page = CreateFrame("Frame", nil, parent)
    page:SetPoint("TOPLEFT", 0, -TB)
    page:SetPoint("TOPRIGHT", 0, -TB)
    page:SetHeight(pageH)
    page:Hide()
    self.pages[key] = page
    return page
end

local function StepScale(cur, delta, minV, maxV)
    local ns = math.max(minV, math.min(maxV, cur + delta))
    return math.floor(ns * 10 + 0.5) / 10
end

local function CreateScaleSpinner(self, parent, configKey, minV, maxV, defVal, onApply)
    local c = VC.Theme:GetScheme()

    local label = parent:CreateFontString(nil, "OVERLAY")
    label:SetWidth(36)
    label:SetJustifyH("CENTER")
    label:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    local curVal = VC.Store:GetState().config[configKey] or defVal
    label:SetText(string.format("%.0f%%", curVal * 100))
    label:SetTextColor(c.accent.r, c.accent.g, c.accent.b, 1)
    VC.Theme:Register(label, "AccentText")
    self._spinnerLabels[configKey] = label

    local function applyStep(delta)
        local ns = StepScale(VC.Store:GetState().config[configKey] or defVal, delta, minV, maxV)
        VC.Store:Dispatch("SET_CONFIG", { key = configKey, value = ns })
        label:SetText(string.format("%.0f%%", ns * 100))
        if onApply then onApply(ns) end
    end

    local down = VC.UI:CreateButton(parent, "-", 24, 22, function() applyStep(-0.1) end)
    label:SetPoint("LEFT", down, "RIGHT", 4, 0)
    local up = VC.UI:CreateButton(parent, "+", 24, 22, function() applyStep(0.1) end)
    up:SetPoint("LEFT", label, "RIGHT", 4, 0)

    return down, label, up
end

local function AddSep(parent, y)
    local sep = VC.UI:CreateSeparator(parent)
    sep:SetPoint("TOPLEFT", 8, y); sep:SetPoint("TOPRIGHT", -8, y)
    return sep
end

local function AddDimHeader(parent, text, x, y)
    local fs = parent:CreateFontString(nil, "OVERLAY")
    fs:SetPoint("TOPLEFT", x, y)
    fs:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    fs:SetText(text)
    local c = VC.Theme:GetScheme()
    fs:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    VC.Theme:Register(fs, "DimText")
    return fs
end

-- ============================================================================
-- INITIALIZATION
-- ============================================================================

function VC.Settings:Initialize()
    -- Lazy build: panel created on first Toggle()
end

function VC.Settings:Toggle()
    if InCombatLockdown() then return end
    if not self.frame then self:Build() end
    local f = self.frame
    if f:IsShown() then
        if not f._closeAG then
            local ag = f:CreateAnimationGroup()
            local a = ag:CreateAnimation("Alpha")
            a:SetFromAlpha(1); a:SetToAlpha(0); a:SetDuration(0.15); a:SetSmoothing("IN")
            ag:SetScript("OnFinished", function() f:Hide(); f:SetAlpha(1) end)
            f._closeAG = ag
        end
        if f._openAG and f._openAG:IsPlaying() then f._openAG:Stop() end
        f._closeAG:Play()
    else
        f:Show(); f:SetAlpha(0)
        if not f._openAG then
            local ag = f:CreateAnimationGroup()
            local a = ag:CreateAnimation("Alpha")
            a:SetFromAlpha(0); a:SetToAlpha(1); a:SetDuration(0.2); a:SetSmoothing("OUT")
            ag:SetScript("OnFinished", function() f:SetAlpha(1) end)
            f._openAG = ag
        end
        if f._closeAG and f._closeAG:IsPlaying() then f._closeAG:Stop(); f:SetAlpha(1) end
        f._openAG:Play()
        self:SelectTab(self.activeTab or "companion")
        self:Refresh()
    end
end

-- ============================================================================
-- BUILD
-- ============================================================================

function VC.Settings:Build()
    local S    = VC.Constants.SETTINGS
    local W    = S.WIDTH         -- 760
    local H    = S.HEIGHT        -- 580
    local TH   = S.TITLE_BAR_H  -- 32
    local TB   = S.TAB_BAR_H    -- 28
    local SW   = S.SIDEBAR_W    -- 180
    local FH   = S.FOOTER_H     -- 20
    local c    = VC.Theme:GetScheme()

    -- -------------------------------------------------------------------------
    -- Main frame
    -- -------------------------------------------------------------------------
    local f = CreateFrame("Frame", "VC_SettingsFrame", UIParent, "BackdropTemplate")
    f:SetSize(W, H)
    f:SetPoint("CENTER")
    f:SetFrameStrata("MEDIUM")
    f:SetMovable(true)
    f:SetClampedToScreen(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)
    f:SetBackdrop(VC.Theme.BACKDROP_PANEL)
    f:SetBackdropColor(c.bg.r, c.bg.g, c.bg.b, c.bg.a)
    f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 1)
    tinsert(UISpecialFrames, "VC_SettingsFrame")
    self.frame = f
    VC.Theme:Register(f, "Frame")

    -- Exit toast move mode when Settings closes
    f:HookScript("OnHide", function()
        if VC.Display and VC.Display._moveMode then
            VC.Display:ExitMoveMode()
            if self._moveToastBtn then
                VC.UI:SetPillActive(self._moveToastBtn, false)
                self._moveToastBtn:GetFontString():SetText("Move Toast")
            end
        end
    end)

    -- -------------------------------------------------------------------------
    -- Title bar
    -- -------------------------------------------------------------------------
    local titleBar = CreateFrame("Frame", nil, f)
    titleBar:SetPoint("TOPLEFT", 0, 0)
    titleBar:SetPoint("TOPRIGHT", 0, 0)
    titleBar:SetHeight(TH)

    local titleGrad = titleBar:CreateTexture(nil, "BACKGROUND")
    titleGrad:SetAllPoints()
    titleGrad:SetTexture(WHITE)
    titleGrad:SetVertexColor(c.accent.r, c.accent.g, c.accent.b, 0.08)


    local titleSep = titleBar:CreateTexture(nil, "ARTWORK")
    titleSep:SetHeight(1)
    titleSep:SetPoint("BOTTOMLEFT", 0, 0); titleSep:SetPoint("BOTTOMRIGHT", 0, 0)
    titleSep:SetTexture(WHITE)
    titleSep:SetVertexColor(c.border.r, c.border.g, c.border.b, 1)
    titleSep._alpha = 1.0; VC.Theme:Register(titleSep, "Separator")

    local titleText = titleBar:CreateFontString(nil, "OVERLAY")
    titleText:SetPoint("LEFT", 10, 0)
    titleText:SetFont("Fonts\\MORPHEUS.ttf", 14, "")
    titleText:SetText("Vamoose's Companion")
    titleText:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, 1)
    titleText:SetShadowOffset(1, -1); titleText:SetShadowColor(0, 0, 0, 1)
    titleText.isHeader = true; VC.Theme:Register(titleText, "Text")

    local closeBtn = CreateFrame("Button", nil, titleBar)
    closeBtn:SetSize(16, 16)
    closeBtn:SetPoint("RIGHT", titleBar, "RIGHT", -6, 0)
    local closeIcon = closeBtn:CreateTexture(nil, "OVERLAY")
    closeIcon:SetAllPoints(); closeIcon:SetAtlas("XMarksTheSpot")
    closeIcon:SetVertexColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    VC.Theme:Register(closeIcon, "DimText")

    closeBtn:SetScript("OnClick", function() f:Hide() end)
    closeBtn:SetScript("OnEnter", function() local s = VC.Theme:GetScheme(); closeIcon:SetVertexColor(s.text_header.r, s.text_header.g, s.text_header.b, 1) end)
    closeBtn:SetScript("OnLeave", function() local s = VC.Theme:GetScheme(); closeIcon:SetVertexColor(s.text_dim.r, s.text_dim.g, s.text_dim.b, 1) end)
    f.titleBar = titleBar

    -- -------------------------------------------------------------------------
    -- Footer (full-width, 20px, at very bottom)
    -- -------------------------------------------------------------------------
    local footerSep = f:CreateTexture(nil, "ARTWORK")
    footerSep:SetHeight(1)
    footerSep:SetPoint("BOTTOMLEFT", 0, FH)
    footerSep:SetPoint("BOTTOMRIGHT", 0, FH)
    footerSep:SetTexture(WHITE)
    footerSep:SetVertexColor(c.border.r, c.border.g, c.border.b, 0.3)
    footerSep._alpha = 0.3; VC.Theme:Register(footerSep, "Separator")


    local version = C_AddOns and C_AddOns.GetAddOnMetadata("VamoosesCompanion", "Version") or "Dev"
    local footerL = f:CreateFontString(nil, "OVERLAY")
    footerL:SetPoint("BOTTOMLEFT", 8, 4)
    footerL:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    footerL:SetText("v" .. version)
    footerL:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    VC.Theme:Register(footerL, "Text")

    local footerR = f:CreateFontString(nil, "OVERLAY")
    footerR:SetPoint("BOTTOMRIGHT", -8, 4)
    footerR:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    footerR:SetText("/vc help for commands")
    footerR:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    VC.Theme:Register(footerR, "Text")
    self.footerText = footerL

    -- -------------------------------------------------------------------------
    -- Vertical divider between sidebar and content
    -- -------------------------------------------------------------------------
    local divider = f:CreateTexture(nil, "ARTWORK")
    divider:SetWidth(1)
    divider:SetPoint("TOPLEFT", SW, -TH)
    divider:SetPoint("BOTTOMLEFT", SW, FH)
    divider:SetTexture(WHITE)
    divider:SetVertexColor(c.border.r, c.border.g, c.border.b, 0.8)
    divider._alpha = 0.8; VC.Theme:Register(divider, "Separator")


    -- -------------------------------------------------------------------------
    -- Left sidebar
    -- -------------------------------------------------------------------------
    local sidebarH = H - TH - FH  -- 528
    local sidebar = CreateFrame("Frame", nil, f)
    sidebar:SetPoint("TOPLEFT", 0, -TH)
    sidebar:SetSize(SW, sidebarH)

    -- Slightly lighter bg than main frame
    local sidebarBg = sidebar:CreateTexture(nil, "BACKGROUND")
    sidebarBg:SetAllPoints()
    sidebarBg:SetTexture(WHITE)
    sidebarBg:SetVertexColor(c.panel.r, c.panel.g, c.panel.b, 0.6)
    self._sidebarBg = sidebarBg

    self.sidebar = sidebar

    -- "COMPANIONS" header
    local sideHeader = sidebar:CreateFontString(nil, "OVERLAY")
    sideHeader:SetPoint("TOPLEFT", 10, -12)
    sideHeader:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    sideHeader:SetText("COMPANIONS")
    sideHeader:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    VC.Theme:Register(sideHeader, "DimText")


    -- Sidebar scroll area (TOPLEFT at -28, BOTTOMRIGHT at +20 above footer)
    local sideScroll = CreateFrame("ScrollFrame", nil, sidebar)
    sideScroll:SetPoint("TOPLEFT", 0, -28)
    sideScroll:SetPoint("BOTTOMRIGHT", 0, 4)
    sideScroll:EnableMouseWheel(true)

    local sideContent = CreateFrame("Frame", nil, sideScroll)
    sideContent:SetWidth(SW)
    sideScroll:SetScrollChild(sideContent)
    sideScroll:SetScript("OnMouseWheel", function(self, delta)
        local cur = self:GetVerticalScroll()
        local maxS = math.max(0, sideContent:GetHeight() - self:GetHeight())
        self:SetVerticalScroll(math.max(0, math.min(maxS, cur - delta * 40)))
    end)
    self._sideContent = sideContent

    -- -------------------------------------------------------------------------
    -- Right content area
    -- -------------------------------------------------------------------------
    local contentX = SW + 1
    local contentW = W - contentX  -- 579
    local contentH = H - TH        -- 548 (footer is handled separately, content reaches it)

    local contentFrame = CreateFrame("Frame", nil, f)
    contentFrame:SetPoint("TOPLEFT", contentX, -TH)
    contentFrame:SetSize(contentW, contentH)
    self.contentFrame = contentFrame

    -- Tab bar (28px inside content frame)
    local tabBar = CreateFrame("Frame", nil, contentFrame)
    tabBar:SetPoint("TOPLEFT", 0, 0)
    tabBar:SetPoint("TOPRIGHT", 0, 0)
    tabBar:SetHeight(TB)

    local tabSep = tabBar:CreateTexture(nil, "ARTWORK")
    tabSep:SetHeight(1)
    tabSep:SetPoint("BOTTOMLEFT", 0, 0); tabSep:SetPoint("BOTTOMRIGHT", 0, 0)
    tabSep:SetTexture(WHITE)
    tabSep:SetVertexColor(c.border.r, c.border.g, c.border.b, 0.6)
    tabSep._alpha = 0.6; VC.Theme:Register(tabSep, "Separator")

    local TAB_DEFS = {
        { key = "companion", label = "Companion" },
        { key = "display",   label = "Display"   },
        { key = "pepe",      label = "Pepe"      },
        { key = "events",    label = "Events"    },
        { key = "locale",    label = "Locale"    },
        { key = "history",   label = "History"   },
        { key = "info",      label = "Info"      },
    }
    self.tabs = {}
    local tabX = 0
    local tabPad = 16
    for _, def in ipairs(TAB_DEFS) do
        local btn = VC.UI:CreateTabButton(tabBar, def.label, function()
            self:SelectTab(def.key)
        end)
        local fs = btn:GetFontString()
        local textW = fs and fs:GetStringWidth() or 60
        local btnW = math.max(60, math.floor(textW + tabPad * 2))
        btn:SetWidth(btnW)
        btn:SetPoint("LEFT", tabX, 0)
        btn._tabKey = def.key
        self.tabs[def.key] = btn
        tabX = tabX + btnW
    end
    f.tabBar = tabBar

    -- Content pages area (below tab bar)
    local pageH = contentH - TB - FH  -- 500
    self.pages = {}
    self._spinnerLabels = {}

    self:BuildCompanionPage(contentFrame, contentW, pageH)
    self:BuildDisplayPage(contentFrame, contentW, pageH)
    self:BuildPepePage(contentFrame, contentW, pageH)
    self:BuildEventsPage(contentFrame, contentW, pageH)
    self:BuildLocalePage(contentFrame, contentW, pageH)
    self:BuildHistoryPage(contentFrame, contentW, pageH)
    self:BuildInfoPage(contentFrame, contentW, pageH)

    -- -------------------------------------------------------------------------
    -- Populate sidebar companion rows (after pages are built)
    -- -------------------------------------------------------------------------
    self:BuildSidebarRows()

    -- Listen for state changes
    VC.EventBus:Register("VC_STATE_CHANGED", function()
        if self.frame and self.frame:IsShown() then
            self:Refresh()
        end
    end)

    f:Hide()
end

-- ============================================================================
-- SIDEBAR ROW CONSTRUCTION
-- ============================================================================

function VC.Settings:BuildSidebarRows()
    local c = VC.Theme:GetScheme()
    local SW = VC.Constants.SETTINGS.SIDEBAR_W
    local ROW_H = 84
    local content = self._sideContent

    -- Explicit display order
    local ORDER = { "vamoose", "scurry", "gazlowe", "patches", "murgle" }
    local keys = {}
    for _, k in ipairs(ORDER) do
        if VC_CompanionProfiles[k] and VC_QuoteDB and VC_QuoteDB[k] then
            keys[#keys + 1] = k
        end
    end

    self._sidebarRows = {}

    local y = 0
    for _, key in ipairs(keys) do
        local profile = VC_CompanionProfiles[key]
        if profile then
            local row = self:_BuildSidebarRow(content, key, profile, ROW_H, SW, c)
            row:SetPoint("TOPLEFT", 0, y)
            self._sidebarRows[key] = row
            y = y - ROW_H - 2
        end
    end

    content:SetHeight(math.max(1, math.abs(y) + 2))
end

function VC.Settings:_BuildSidebarRow(parent, key, profile, ROW_H, SW, c)
    local row = CreateFrame("Button", nil, parent)
    row:SetSize(SW, ROW_H)
    row:EnableMouse(true)
    row._companionKey = key

    -- Row background (transparent default, hover fill). Sublevel -1 to avoid
    -- painting over adjacent row backdrop borders.
    local rowBg = row:CreateTexture(nil, "BACKGROUND", nil, -1)
    rowBg:SetAllPoints()
    rowBg:SetTexture(WHITE)
    rowBg:SetVertexColor(0, 0, 0, 0)
    row._bg = rowBg

    -- Left accent bar (3px, hidden unless selected-for-editing)
    local accentBar = row:CreateTexture(nil, "ARTWORK")
    accentBar:SetWidth(3)
    accentBar:SetPoint("TOPLEFT", 0, 0)
    accentBar:SetPoint("BOTTOMLEFT", 0, 0)
    accentBar:SetTexture(WHITE)
    local tc = profile.themeColor or { r = 0.302, g = 0.788, b = 0.965 }
    accentBar:SetVertexColor(tc.r, tc.g, tc.b, 0)
    row._accentBar = accentBar
    row._themeColor = tc

    -- 3D preview card (BackdropTemplate for bg + border, 5px padding in 84px row)
    local previewFrame = CreateFrame("Frame", nil, row, "BackdropTemplate")
    previewFrame:SetSize(ROW_H - 10, ROW_H - 10)
    previewFrame:SetPoint("LEFT", row, "LEFT", 8, 0)
    previewFrame:SetBackdrop(VC.Theme.BACKDROP_INSET)
    previewFrame:SetBackdropColor(c.bg.r, c.bg.g, c.bg.b, 1)
    previewFrame:SetBackdropBorderColor(tc.r, tc.g, tc.b, 0.5)
    row._previewFrame = previewFrame

    local previewScene = VC.UI:CreateModelScene(previewFrame, 0)
    previewScene:SetPoint("TOPLEFT", 2, -2)
    previewScene:SetPoint("BOTTOMRIGHT", -2, 2)
    previewScene:EnableMouse(false)
    -- Zoom camera in for small creatures (squirrels, critters); sidebarCamera overrides
    if profile.sidebarCamera then
        previewScene:SetCameraPosition(profile.sidebarCamera, 0, 0)
    elseif profile.portraitScale and profile.portraitScale < 0.5 then
        previewScene:SetCameraPosition(0.8, 0, 0)
    end
    row._previewScene = previewScene

    local displayID = profile.displayID or 19598
    VC.UI:SetCreatureOnScene(previewScene, displayID, { scale = 0.9 })

    -- Name label (LEFT of preview RIGHT+8, TOP-aligned at -10 from row top)
    local nameLabel = row:CreateFontString(nil, "OVERLAY")
    nameLabel:SetPoint("TOPLEFT", previewFrame, "TOPRIGHT", 8, -10)
    nameLabel:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    nameLabel:SetText(profile.name)
    nameLabel:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, 1)
    nameLabel.isHeader = true; VC.Theme:Register(nameLabel, "Text")

    -- Title label (below name, gap 3)

    -- TTS toggle (bottom-left of preview card, above sound icon)
    local ttsBtn = CreateFrame("Button", nil, previewFrame)
    ttsBtn:SetSize(14, 14)
    ttsBtn:SetPoint("BOTTOMLEFT", 2, 18)
    ttsBtn:SetFrameLevel(previewFrame:GetFrameLevel() + 5)
    local ttsIcon = ttsBtn:CreateTexture(nil, "ARTWORK")
    ttsIcon:SetAllPoints()
    ttsIcon:SetAtlas("chatframe-button-icon-TTS")
    row._ttsIcon = ttsIcon
    row._ttsBtn = ttsBtn

    local function UpdateRowTtsIcon()
        local s = VC.Store:GetState()
        local on = s.config.ttsEnabled and s.config.companionTts[key] ~= false
        ttsIcon:SetDesaturated(not on)
        ttsIcon:SetAlpha(on and 1 or 0.5)
    end
    UpdateRowTtsIcon()
    row._updateTtsIcon = UpdateRowTtsIcon

    ttsBtn:SetScript("OnClick", function()
        local s = VC.Store:GetState()
        if not s.config.ttsEnabled then return end
        local tbl = {}
        for k, v in pairs(s.config.companionTts) do tbl[k] = v end
        tbl[key] = not (tbl[key] ~= false)
        VC.Store:Dispatch("SET_CONFIG", { key = "companionTts", value = tbl })
        UpdateRowTtsIcon()
    end)
    ttsBtn:SetScript("OnEnter", function(btn)
        GameTooltip:SetOwner(btn, "ANCHOR_RIGHT")
        if not VC.Store:GetState().config.ttsEnabled then
            GameTooltip:SetText("TTS disabled (enable in Display)")
        else
            local s = VC.Store:GetState()
            local on = s.config.companionTts[key] ~= false
            GameTooltip:SetText(on and "TTS: ON" or "TTS: OFF")
        end
        GameTooltip:Show()
    end)
    ttsBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Speaker toggle (bottom-left of preview card)
    local sndBtn = CreateFrame("Button", nil, previewFrame)
    sndBtn:SetSize(14, 14)
    sndBtn:SetPoint("BOTTOMLEFT", 2, 2)
    sndBtn:SetFrameLevel(previewFrame:GetFrameLevel() + 5)
    local sndIcon = sndBtn:CreateTexture(nil, "ARTWORK")
    sndIcon:SetAllPoints()
    row._sndIcon = sndIcon
    row._sndBtn = sndBtn

    -- Check if any model group has actual sound data
    local hasSounds = false
    if profile.modelGroups then
        for _, g in ipairs(profile.modelGroups) do
            if g.intents then
                for _, entry in pairs(g.intents) do
                    if entry.sound and #entry.sound > 0 then hasSounds = true; break end
                end
            end
            if hasSounds then break end
        end
    end

    local function UpdateRowSoundIcon()
        if not hasSounds then
            sndIcon:SetAtlas("voicechat-icon-speaker-silenced")
            return
        end
        local s = VC.Store:GetState()
        local on = s.config.companionSound[key] ~= false
        sndIcon:SetAtlas(on and "voicechat-icon-speaker" or "voicechat-icon-speaker-mute")
    end
    UpdateRowSoundIcon()
    row._updateSoundIcon = UpdateRowSoundIcon

    sndBtn:SetScript("OnClick", function()
        if not hasSounds then return end
        local s = VC.Store:GetState()
        local tbl = {}
        for k, v in pairs(s.config.companionSound) do tbl[k] = v end
        tbl[key] = not (tbl[key] ~= false)
        VC.Store:Dispatch("SET_CONFIG", { key = "companionSound", value = tbl })
        UpdateRowSoundIcon()
    end)
    sndBtn:SetScript("OnEnter", function(btn)
        GameTooltip:SetOwner(btn, "ANCHOR_RIGHT")
        if not hasSounds then
            GameTooltip:SetText("No sounds available")
        else
            local s = VC.Store:GetState()
            local on = s.config.companionSound[key] ~= false
            GameTooltip:SetText(on and "Sounds: ON" or "Sounds: OFF")
        end
        GameTooltip:Show()
    end)
    sndBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    -- Active companion indicator dot (TOPRIGHT, 6px cyan dot)
    local activeDot = row:CreateTexture(nil, "OVERLAY")
    activeDot:SetSize(6, 6)
    activeDot:SetPoint("TOPRIGHT", row, "TOPRIGHT", -6, -6)
    activeDot:SetTexture(WHITE)
    activeDot:SetVertexColor(c.accent.r, c.accent.g, c.accent.b, 1)
    activeDot:Hide()
    VC.Theme:Register(activeDot, "AccentText")
    row._activeDot = activeDot

    -- Row divider at bottom
    local rowDiv = row:CreateTexture(nil, "ARTWORK")
    rowDiv:SetHeight(1)
    rowDiv:SetPoint("BOTTOMLEFT", 0, 0); rowDiv:SetPoint("BOTTOMRIGHT", 0, 0)
    rowDiv:SetTexture(WHITE)
    rowDiv:SetVertexColor(c.border.r, c.border.g, c.border.b, 0.3)
    rowDiv._alpha = 0.3; VC.Theme:Register(rowDiv, "Separator")

    -- Click: set as active companion + select for editing
    row:SetScript("OnClick", function()
        VC.Store:Dispatch("SET_CONFIG", { key = "activeCompanion", value = key })
        if VC.Portrait and VC.Portrait.UpdateModel then VC.Portrait:UpdateModel(true) end
        self:SelectEditingCompanion(key)
    end)

    row:SetScript("OnEnter", function(r)
        if not r._editing then
            local s = VC.Theme:GetScheme()
            r._bg:SetVertexColor(s.panel_alt and s.panel_alt.r or 0.067,
                                 s.panel_alt and s.panel_alt.g or 0.122,
                                 s.panel_alt and s.panel_alt.b or 0.220, 0.5)
        end
    end)
    row:SetScript("OnLeave", function(r)
        if not r._editing then
            r._bg:SetVertexColor(0, 0, 0, 0)
        end
    end)

    return row
end

-- Select a companion for editing (updates right panel, does not change active companion)
function VC.Settings:SelectEditingCompanion(key)
    self._editingKey = key

    for rowKey, row in pairs(self._sidebarRows or {}) do
        local tc = row._themeColor or { r = 0.302, g = 0.788, b = 0.965 }
        if rowKey == key then
            row._editing = true
            row._accentBar:SetVertexColor(tc.r, tc.g, tc.b, 1)
            row._bg:SetVertexColor(tc.r, tc.g, tc.b, 0.06)
            row._previewFrame:SetBackdropBorderColor(tc.r, tc.g, tc.b, 0.9)
        else
            row._editing = false
            row._accentBar:SetVertexColor(tc.r, tc.g, tc.b, 0)
            row._bg:SetVertexColor(0, 0, 0, 0)
            row._previewFrame:SetBackdropBorderColor(tc.r, tc.g, tc.b, 0.5)
        end
    end

    -- Update right panel companion content
    self:RefreshCompanionPage(key)
end

-- ============================================================================
-- TAB SELECTION
-- ============================================================================

function VC.Settings:SelectTab(key)
    self.activeTab = key
    for tabKey, page in pairs(self.pages) do
        if tabKey == key then
            page:SetAlpha(1)
            page:Show()
        else
            page:Hide()
        end
    end
    for tabKey, btn in pairs(self.tabs) do
        VC.UI:SetTabActive(btn, tabKey == key)
    end
    if key == "history" then self:RefreshHistory() end
    if key == "pepe" then self:RefreshPepePage() end
    if key == "locale" then self:RefreshLocalePage() end
end

-- ============================================================================
-- DISPLAY PAGE (wider layout)
-- ============================================================================

function VC.Settings:BuildDisplayPage(parent, W, pageH)
    local c = VC.Theme:GetScheme()
    local page = MakePage(self, parent, pageH, "display")

    local y = -12

    -- Chattiness section
    AddDimHeader(page, "Chattiness", 12, y)
    y = y - 18

    local segOptions = {
        { key = "1", label = "Minimal" },
        { key = "2", label = "Relaxed" },
        { key = "3", label = "Chatty"  },
        { key = "4", label = "Maximum" },
    }
    local segGroup = VC.UI:CreateRadioGroup(page, segOptions, function(selKey)
        local level = tonumber(selKey)
        if level then VC.Store:Dispatch("SET_CHATTINESS", { level = level }) end
    end)
    segGroup:SetPoint("TOPLEFT", 12, y)
    self.chattinessGroup = segGroup
    y = y - 28

    local chatDesc = page:CreateFontString(nil, "OVERLAY")
    chatDesc:SetPoint("TOPLEFT", 12, y)
    chatDesc:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    chatDesc:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    chatDesc:SetText("")
    self.chattinessDesc = chatDesc
    VC.Theme:Register(chatDesc, "DimText")
    y = y - 18

    AddSep(page, y)
    y = y - 10

    -- Display Mode section
    AddDimHeader(page, "Display Mode", 12, y)
    y = y - 18

    self.modeBtns = {}
    local modeOpts = { { key = "bubble", label = "Talking Head" }, { key = "chat", label = "Chat" } }
    local modeX = 12
    for _, mode in ipairs(modeOpts) do
        local btn = VC.UI:CreatePillButton(page, mode.label, 110, 22, function()
            VC.Store:Dispatch("TOGGLE_DISPLAY_MODE", { mode = mode.key })
            self:RefreshDisplayPage()
        end)
        btn:SetPoint("TOPLEFT", modeX, y)
        btn._modeKey = mode.key
        self.modeBtns[mode.key] = btn
        modeX = modeX + 116
    end

    -- Mute checkbox (right of mode buttons, same row)
    self.muteCheck = VC.UI:CreateCheckbox(page, "Mute all quotes", function(checked)
        VC.Store:Dispatch("SET_MUTED", { muted = checked })
    end)
    self.muteCheck:SetPoint("TOPLEFT", modeX + 8, y)
    y = y - 26

    -- TTS checkbox (speak quotes aloud via VamoosesTTS)
    self._ttsCB = VC.UI:CreateCheckbox(page, "Speak quotes aloud (TTS)", function(checked)
        VC.Store:Dispatch("SET_CONFIG", { key = "ttsEnabled", value = checked })
    end)
    self._ttsCB:SetPoint("TOPLEFT", 12, y)

    -- Dim hint if VamoosesTTS not installed
    if not VamoosesTTS then
        self._ttsCB.checkbox:Disable()
        self._ttsCB.label:SetTextColor(0.4, 0.4, 0.4, 1)
        local hint = page:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        hint:SetPoint("LEFT", self._ttsCB.label, "RIGHT", 6, 0)
        hint:SetText("|cFF888888(install Vamoose's TTS)|r")
    end
    y = y - 30

    AddSep(page, y)
    y = y - 10

    -- Portrait Frame style + scale (on same row)
    AddDimHeader(page, "Portrait Frame", 12, y)
    y = y - 18

    local frameOpts = {
        { key = "atlas",   label = "Classic" },
        { key = "ornate",  label = "Ornate"  },
        { key = "minimal", label = "Minimal" },
    }
    local frameGroup = VC.UI:CreateRadioGroup(page, frameOpts, function(selKey)
        VC.Store:Dispatch("SET_CONFIG", { key = "portraitFrame", value = selKey })
        if VC.Portrait and VC.Portrait.frame then
            VC.UI:ApplyPortraitFrameStyle(VC.Portrait.frame)
        end
    end)
    frameGroup:SetPoint("TOPLEFT", 12, y)
    self._frameGroup = frameGroup

    -- Portrait scale controls (right of segmented group)
    local pScaleDown = CreateScaleSpinner(self, page, "portraitScale", 0.6, 1.6, 1.0, function(ns)
        if VC.Portrait and VC.Portrait.frame and not InCombatLockdown() then VC.Portrait.frame:SetScale(ns) end
    end)
    pScaleDown:SetPoint("TOPLEFT", 294, y)
    y = y - 28

    -- Toast Frame style + scale (on same row)
    AddDimHeader(page, "Toast Frame", 12, y)
    y = y - 18

    local toastGroup = VC.UI:CreateRadioGroup(page, frameOpts, function(selKey)
        VC.Store:Dispatch("SET_CONFIG", { key = "toastFrame", value = selKey })
        if VC.Display and VC.Display.talkingHead then
            VC.Display:ApplyToastStyle(VC.Display.talkingHead)
        end
    end)
    toastGroup:SetPoint("TOPLEFT", 12, y)
    self._toastGroup = toastGroup

    -- Toast scale controls (right of segmented group)
    local tScaleDown = CreateScaleSpinner(self, page, "toastScale", 0.5, 1.6, 1.0, function()
        if VC.Display and VC.Display.talkingHead then VC.Display:ApplyToastStyle(VC.Display.talkingHead) end
    end)
    tScaleDown:SetPoint("TOPLEFT", 294, y)
    y = y - 28

    AddSep(page, y)
    y = y - 10

    -- Theme label + name
    local themeRowLabel = AddDimHeader(page, "Theme:", 12, y)

    local themeNameLabel = page:CreateFontString(nil, "OVERLAY")
    themeNameLabel:SetPoint("LEFT", themeRowLabel, "RIGHT", 5, 0)
    themeNameLabel:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    themeNameLabel:SetTextColor(c.text.r, c.text.g, c.text.b, 1)
    VC.Theme:Register(themeNameLabel, "Text")
    self.themeNameLabel = themeNameLabel
    y = y - 20

    -- Theme color swatches (VPP pattern)
    local swatchSize = 20
    local swatchGap = 3
    local swatchX = 12
    self._themeChips = {}

    for _, themeKey in ipairs(VC.Constants.ThemeOrder) do
        local themeName = VC.Constants.ThemeNames[themeKey]
        local displayName = VC.Constants.ThemeDisplayNames[themeKey] or themeKey
        local themeScheme = VC.Colors.Schemes[themeName]
        local accent = themeScheme and themeScheme.accent

        local btn = CreateFrame("Button", nil, page)
        btn:SetSize(swatchSize, swatchSize)
        btn:SetPoint("TOPLEFT", swatchX, y)
        swatchX = swatchX + swatchSize + swatchGap

        local fill = btn:CreateTexture(nil, "ARTWORK")
        fill:SetPoint("TOPLEFT", 2, -2)
        fill:SetPoint("BOTTOMRIGHT", -2, 2)
        fill:SetTexture(WHITE)
        if accent then fill:SetVertexColor(accent.r, accent.g, accent.b, 1) end

        local border = btn:CreateTexture(nil, "BORDER")
        border:SetPoint("TOPLEFT", -1, 1)
        border:SetPoint("BOTTOMRIGHT", 1, -1)
        border:SetTexture(WHITE)
        border:SetVertexColor(1, 1, 1, 0.9)
        border:Hide()
        btn._fill = fill
        btn._border = border

        btn:SetScript("OnClick", function()
            local newTheme = themeKey
            VC.Store:Dispatch("SET_CONFIG", { key = "theme", value = newTheme })
            VC.Constants:ApplyTheme()
            VC.EventBus:Trigger("VC_THEME_UPDATE", { themeName = VC.Constants.ThemeNames[newTheme] })
            self:Refresh()
        end)
        btn:SetScript("OnEnter", function(self)
            if accent then self._fill:SetVertexColor(accent.r, accent.g, accent.b, 0.7) end
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetText(displayName, 1, 1, 1)
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", function(self)
            if accent then self._fill:SetVertexColor(accent.r, accent.g, accent.b, 1) end
            GameTooltip:Hide()
        end)

        self._themeChips[themeKey] = btn
    end
    y = y - 26

    -- =========================================================================
    -- Toast Position + Duration (combined row)
    -- =========================================================================
    AddSep(page, y)
    y = y - 12

    local toastPosHeader = AddDimHeader(page, "Toast Position", 12, y)

    local toastPosLabel = page:CreateFontString(nil, "OVERLAY")
    toastPosLabel:SetPoint("LEFT", toastPosHeader, "RIGHT", 10, 0)
    toastPosLabel:SetFont("Fonts\\FRIZQT__.TTF", 11, "")
    toastPosLabel:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 0.7)
    VC.Theme:Register(toastPosLabel, "DimText")
    self._toastPosLabel = toastPosLabel

    -- Toast Duration (+/- inline, right side of same header row)
    local durLabel = AddDimHeader(page, "Duration", 260, y)

    local durDown = VC.UI:CreateButton(page, "-", 24, 22, function()
        local cur = VC.Store:GetState().config.toastDuration or 6
        local ns = math.max(cur - 1, 2)
        VC.Store:Dispatch("SET_CONFIG", { key = "toastDuration", value = ns })
        self._toastDurValue:SetText(ns .. "s")
    end)
    durDown:SetPoint("TOPLEFT", 320, y)

    local durValue = page:CreateFontString(nil, "OVERLAY")
    durValue:SetPoint("LEFT", durDown, "RIGHT", 4, 0)
    durValue:SetWidth(28)
    durValue:SetJustifyH("CENTER")
    durValue:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    local curDur = VC.Store:GetState().config.toastDuration or 6
    durValue:SetText(curDur .. "s")
    durValue:SetTextColor(c.accent.r, c.accent.g, c.accent.b, 1)
    VC.Theme:Register(durValue, "AccentText")
    self._toastDurValue = durValue

    local durUp = VC.UI:CreateButton(page, "+", 24, 22, function()
        local cur = VC.Store:GetState().config.toastDuration or 6
        local ns = math.min(cur + 1, 15)
        VC.Store:Dispatch("SET_CONFIG", { key = "toastDuration", value = ns })
        self._toastDurValue:SetText(ns .. "s")
    end)
    durUp:SetPoint("LEFT", durValue, "RIGHT", 4, 0)
    y = y - 22

    local moveBtn = VC.UI:CreatePillButton(page, "Move Toast", 100, 22, function()
        if not VC.Display then return end
        local btn = VC.Settings._moveToastBtn
        if VC.Display._moveMode then
            VC.Display:ExitMoveMode()
            VC.UI:SetPillActive(btn, false)
            btn:GetFontString():SetText("Move Toast")
        else
            VC.Display:EnterMoveMode()
            VC.UI:SetPillActive(btn, true)
            btn:GetFontString():SetText("Done")
        end
        VC.Settings:RefreshToastPosLabel()
    end)
    moveBtn:SetPoint("TOPLEFT", 12, y)
    self._moveToastBtn = moveBtn

    local resetBtn = VC.UI:CreatePillButton(page, "Reset", 80, 22, function()
        local curStyle = VC.Store:GetState().config.toastFrame or "atlas"
        VC.Store:Dispatch("SET_TOAST_POSITION", { style = curStyle, point = nil })
        if VC.Display and VC.Display._moveMode and VC.Display.talkingHead then
            VC.Display:ApplyToastStyle(VC.Display.talkingHead)
            local companion = VC_CompanionProfiles and VC_CompanionProfiles[VC.Store:GetState().config.activeCompanion]
            VC.Display.talkingHead.nameText:SetText(companion and companion.name or "Companion")
            VC.Display.talkingHead.bodyText:SetText("This is where my quotes will appear.")
        end
        self:RefreshToastPosLabel()
    end)
    resetBtn:SetPoint("LEFT", moveBtn, "RIGHT", 8, 0)
    y = y - 28

    -- =========================================================================
    -- Battle Pet Auto-Summon (one line)
    -- =========================================================================
    AddSep(page, y)
    y = y - 12

    AddDimHeader(page, "Battle Pet", 12, y + 2)

    local autoSummonCB = VC.UI:CreateCheckbox(page, "Auto-summon", function(checked)
        VC.Store:Dispatch("SET_CONFIG", { key = "autoSummonEnabled", value = checked })
    end)
    autoSummonCB:SetPoint("TOPLEFT", 100, y)
    self._autoSummonCB = autoSummonCB

    local autoResummonCB = VC.UI:CreateCheckbox(page, "Re-summon after loading", function(checked)
        VC.Store:Dispatch("SET_CONFIG", { key = "autoResummon", value = checked })
    end)
    autoResummonCB:SetPoint("TOPLEFT", 240, y)
    self._autoResummonCB = autoResummonCB
    y = y - 28

    -- =========================================================================
    -- Activity Muting (one line)
    -- =========================================================================
    AddSep(page, y)
    y = y - 12

    AddDimHeader(page, "Mute In", 12, y + 2)

    local muteDungeonCB = VC.UI:CreateCheckbox(page, "Dungeons / Delves", function(checked)
        VC.Store:Dispatch("SET_CONFIG", { key = "muteInDungeons", value = checked })
    end)
    muteDungeonCB:SetPoint("TOPLEFT", 100, y)
    self._muteDungeonCB = muteDungeonCB

    local muteRaidCB = VC.UI:CreateCheckbox(page, "Raids", function(checked)
        VC.Store:Dispatch("SET_CONFIG", { key = "muteInRaids", value = checked })
    end)
    muteRaidCB:SetPoint("TOPLEFT", 240, y)
    self._muteRaidCB = muteRaidCB

    local mutePvPCB = VC.UI:CreateCheckbox(page, "PvP", function(checked)
        VC.Store:Dispatch("SET_CONFIG", { key = "muteInPvP", value = checked })
    end)
    mutePvPCB:SetPoint("TOPLEFT", 310, y)
    self._mutePvPCB = mutePvPCB
end

-- ============================================================================
-- PEPE PAGE (costume grid + settings)
-- ============================================================================

local PEPE_ANCHOR_ID = 93797
local PEPE_COSTUMES_REF  -- populated on first build

function VC.Settings:BuildPepePage(parent, W, pageH)
    local c = VC.Theme:GetScheme()
    local page = MakePage(self, parent, pageH, "pepe")

    PEPE_COSTUMES_REF = VC.Pepe and VC.Pepe:GetCostumes() or {}

    local y = -12

    -- Settings row: Show + Scale
    local showPepeCB = VC.UI:CreateCheckbox(page, "Show Pepe", function(checked)
        VC.Store:Dispatch("SET_CONFIG", { key = "showPepe", value = checked })
    end)
    showPepeCB:SetPoint("TOPLEFT", 12, y)
    self._showPepeCB = showPepeCB

    local hideMinCB = VC.UI:CreateCheckbox(page, "Hide in minimal", function(checked)
        VC.Store:Dispatch("SET_CONFIG", { key = "pepeHideMinimal", value = checked })
    end)
    hideMinCB:SetPoint("TOPLEFT", 140, y)
    self._pepeHideMinCB = hideMinCB

    local randomCB = VC.UI:CreateCheckbox(page, "Random", function(checked)
        VC.Store:Dispatch("SET_CONFIG", { key = "pepeRandomCostume", value = checked })
        if checked and VC.Pepe then VC.Pepe:ApplyCostume() end
        self:RefreshPepePage()
    end)
    randomCB:SetPoint("TOPLEFT", 300, y)
    self._pepeRandomCB = randomCB

    -- Scale controls
    local sclLabel = page:CreateFontString(nil, "OVERLAY")
    sclLabel:SetPoint("TOPLEFT", 400, y + 2)
    sclLabel:SetFont("Fonts\\ARIALN.TTF", 11, "")
    sclLabel:SetText("Scale:")
    sclLabel:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    VC.Theme:Register(sclLabel, "DimText")

    local pepeSclDown = CreateScaleSpinner(self, page, "pepeScale", 0.4, 2.0, 1.0)
    pepeSclDown:SetPoint("LEFT", sclLabel, "RIGHT", 4, 0)
    y = y - 30

    AddSep(page, y)
    y = y - 10

    -- Costume card grid
    local CARD_W, CARD_H = 90, 82
    local COLS = 5
    local PAD = 4
    self._pepeCostumeCards = {}

    for i, costume in ipairs(PEPE_COSTUMES_REF) do
        local col = (i - 1) % COLS
        local row = math.floor((i - 1) / COLS)
        local x = col * (CARD_W + PAD) + 12
        local cy = y - (row * (CARD_H + PAD))

        local card = CreateFrame("Button", nil, page, "BackdropTemplate")
        card:SetSize(CARD_W, CARD_H)
        card:SetPoint("TOPLEFT", x, cy)
        card:SetBackdrop(VC.Theme.BACKDROP_INSET)
        card:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, 1)
        card:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 1)

        -- PlayerModel for SVK rendering
        local model = CreateFrame("PlayerModel", nil, card)
        model:SetSize(CARD_W - 6, CARD_H - 18)
        model:SetPoint("TOP", 0, -3)
        model:EnableMouse(false)

        -- Set up anchor + SVK (apply after model loads)
        local svkID = costume.id
        model:SetScript("OnModelLoaded", function(m)
            m:SetAlpha(0)
            pcall(m.ApplySpellVisualKit, m, svkID, false)
        end)
        pcall(model.SetDisplayInfo, model, PEPE_ANCHOR_ID)
        if model.Undress then pcall(model.Undress, model) end
        pcall(model.SetAnimation, model, 0, 0)
        pcall(model.SetPaused, model, true)
        model:SetCamDistanceScale(0.3)
        model:SetPortraitZoom(0)
        model:SetPosition(0, 0, -0.4)
        model:SetFacing(0)

        -- Collected indicator (top-right corner)
        local check = card:CreateFontString(nil, "OVERLAY")
        check:SetPoint("TOPRIGHT", -2, -2)
        check:SetFont("Fonts\\FRIZQT__.TTF", 12, "OUTLINE")
        check:SetText("|cFF33EE33*|r")
        check:Hide()
        card._check = check

        -- Costume name label
        local label = card:CreateFontString(nil, "OVERLAY")
        label:SetPoint("BOTTOM", 0, 4)
        label:SetFont("Fonts\\ARIALN.TTF", 9, "")
        label:SetText(costume.name)
        label:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)

        local idx = i
        card:SetScript("OnClick", function()
            VC.Store:Dispatch("SET_CONFIG", { key = "pepeCostume", value = idx })
            VC.Store:Dispatch("SET_CONFIG", { key = "pepeRandomCostume", value = false })
            if VC.Pepe then VC.Pepe:ApplyCostume() end
            self._pepeSelectedDetail = idx
            self:RefreshPepePage()
        end)

        card._costumeIdx = i
        card._costume = costume
        card._label = label
        self._pepeCostumeCards[i] = card
    end

    -- Detail panel at bottom
    local totalRows = math.ceil(#PEPE_COSTUMES_REF / COLS)
    local detailY = y - (totalRows * (CARD_H + PAD)) - 6

    local detailSep = page:CreateTexture(nil, "ARTWORK")
    detailSep:SetHeight(1)
    detailSep:SetPoint("TOPLEFT", 8, detailY)
    detailSep:SetPoint("TOPRIGHT", -8, detailY)
    detailSep:SetTexture("Interface\\Buttons\\WHITE8x8")
    detailSep:SetVertexColor(c.border.r, c.border.g, c.border.b, 0.4)

    local detailName = page:CreateFontString(nil, "OVERLAY")
    detailName:SetPoint("TOPLEFT", 12, detailY - 8)
    detailName:SetFont("Fonts\\FRIZQT__.TTF", 13, "")
    detailName:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, 1)
    detailName:SetText("Select a costume")
    self._pepeDetailName = detailName

    local detailStatus = page:CreateFontString(nil, "OVERLAY")
    detailStatus:SetPoint("LEFT", detailName, "RIGHT", 8, 0)
    detailStatus:SetFont("Fonts\\ARIALN.TTF", 11, "")
    detailStatus:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    self._pepeDetailStatus = detailStatus

    local detailSource = page:CreateFontString(nil, "OVERLAY")
    detailSource:SetPoint("TOPLEFT", 12, detailY - 26)
    detailSource:SetPoint("TOPRIGHT", -12, detailY - 26)
    detailSource:SetFont("Fonts\\ARIALN.TTF", 11, "")
    detailSource:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    detailSource:SetJustifyH("LEFT")
    detailSource:SetWordWrap(true)
    self._pepeDetailSource = detailSource

    -- Show first costume detail by default
    self._pepeSelectedDetail = VC.Store:GetState().config.pepeCostume or 1
end

function VC.Settings:RefreshPepePage()
    local state = VC.Store:GetState()
    local c = VC.Theme:GetScheme()

    if self._showPepeCB then self._showPepeCB.checkbox:SetChecked(state.config.showPepe) end
    if self._pepeHideMinCB then self._pepeHideMinCB.checkbox:SetChecked(state.config.pepeHideMinimal ~= false) end
    if self._pepeRandomCB then self._pepeRandomCB.checkbox:SetChecked(state.config.pepeRandomCostume) end
    if self._spinnerLabels.pepeScale then
        local s = state.config.pepeScale or 1.0
        self._spinnerLabels.pepeScale:SetText(string.format("%.0f%%", s * 100))
    end

    -- Highlight active costume card + collected status
    local activeIdx = state.config.pepeCostume or 1
    local isRandom = state.config.pepeRandomCostume
    local collectedCount = 0
    for i, card in ipairs(self._pepeCostumeCards or {}) do
        local collected = VC.Pepe and card._costume and VC.Pepe:IsCostumeCollected(card._costume)
        if collected then collectedCount = collectedCount + 1 end
        card._check:SetShown(collected)

        if not isRandom and i == activeIdx then
            card:SetBackdropColor(c.accent.r, c.accent.g, c.accent.b, 0.25)
            card:SetBackdropBorderColor(c.accent.r, c.accent.g, c.accent.b, 1)
            card._label:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, 1)
        else
            card:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, 1)
            card:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 1)
            card._label:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
        end
    end

    -- Update detail panel
    local costumes = VC.Pepe and VC.Pepe:GetCostumes() or {}
    local detailIdx = self._pepeSelectedDetail or activeIdx
    local costume = costumes[detailIdx]
    if costume and self._pepeDetailName then
        self._pepeDetailName:SetText(costume.name)
        local collected = VC.Pepe and VC.Pepe:IsCostumeCollected(costume)
        if collected then
            self._pepeDetailStatus:SetText("|cFF33EE33Collected|r")
            self._pepeDetailSource:SetText(costume.source or "")
        else
            self._pepeDetailStatus:SetText("|cFFFF6666Not collected|r")
            self._pepeDetailSource:SetText((costume.source or "") .. "\n|cFF94a3b8Vamoose's Companion can display any costume regardless of collection status.|r")
        end
    end
end

function VC.Settings:RefreshToastPosLabel()
    if not self._toastPosLabel then return end
    local state = VC.Store:GetState()
    local style = state.config.toastFrame or "atlas"
    local tp = state.ui.toastPoints[style]
    if tp then
        self._toastPosLabel:SetText("(custom)")
    else
        self._toastPosLabel:SetText("(default)")
    end
end

function VC.Settings:RefreshDisplayPage()
    local state = VC.Store:GetState()

    if self.chattinessGroup then
        self.chattinessGroup:SetValue(tostring(state.config.chattiness))
        local tier = VC.Constants.CHATTINESS[state.config.chattiness]
        if tier and self.chattinessDesc then
            self.chattinessDesc:SetText(tier.label .. " -- global CD " .. tier.globalCD .. "s")
        end
    end

    for key, btn in pairs(self.modeBtns or {}) do
        local isActive = state.config.displayModes and state.config.displayModes[key]
        VC.UI:SetPillActive(btn, isActive)
    end

    if self.muteCheck then self.muteCheck.checkbox:SetChecked(state.config.muted) end
    if self._ttsCB then self._ttsCB.checkbox:SetChecked(state.config.ttsEnabled) end

    -- Update portrait scale display
    if self._spinnerLabels.portraitScale then
        local s = state.config.portraitScale or 1.0
        self._spinnerLabels.portraitScale:SetText(string.format("%.0f%%", s * 100))
    end

    if self._frameGroup then
        self._frameGroup:SetValue(state.config.portraitFrame or "atlas")
    end
    if self._toastGroup then
        self._toastGroup:SetValue(state.config.toastFrame or "atlas")
    end

    local themeKey = state.config.theme or "slipstreamdark"
    local displayName = VC.Constants.ThemeDisplayNames[themeKey] or themeKey
    if self.themeNameLabel then self.themeNameLabel:SetText(displayName) end
    -- Update theme chip borders
    if self._themeChips then
        for key, btn in pairs(self._themeChips) do
            btn._border:SetShown(key == themeKey)
        end
    end

    if self._spinnerLabels.toastScale then
        local ts = state.config.toastScale or 1.0
        self._spinnerLabels.toastScale:SetText(string.format("%.0f%%", ts * 100))
    end
    self:RefreshToastPosLabel()
    if self._autoSummonCB then self._autoSummonCB.checkbox:SetChecked(state.config.autoSummonEnabled) end
    if self._autoResummonCB then self._autoResummonCB.checkbox:SetChecked(state.config.autoResummon) end
    if self._toastDurValue then
        local dur = state.config.toastDuration or 6
        self._toastDurValue:SetText(dur .. "s")
    end
    if self._muteDungeonCB then self._muteDungeonCB.checkbox:SetChecked(state.config.muteInDungeons) end
    if self._muteRaidCB then self._muteRaidCB.checkbox:SetChecked(state.config.muteInRaids) end
    if self._mutePvPCB then self._mutePvPCB.checkbox:SetChecked(state.config.muteInPvP) end
end

-- ============================================================================
-- EVENTS PAGE (3-column toggle layout at 548px inner width)
-- ============================================================================

function VC.Settings:BuildEventsPage(parent, W, pageH)
    local page = MakePage(self, parent, pageH, "events")

    local sf, content = VC.UI:CreateScrollFrame(page, W - 8, pageH - 4)
    sf:SetPoint("TOPLEFT", 4, -2)
    VC.UI.StyleScrollBar(sf)

    local y = 0
    self.eventChecks = {}

    local GROUPS = {
        {
            label = "ADVENTURING",
            events = { "boss", "death", "levelup", "zone", "achieve", "quest", "ambient", "scenario" },
        },
        {
            label = "ECONOMY",
            events = { "gold", "farming", "rep", "reprank", "crafting", "fishing", "mission" },
        },
        {
            label = "SOCIAL",
            events = { "login", "friendonline", "combatend", "group", "afkreturn", "logout" },
        },
        {
            label = "COLLECTIONS",
            events = { "housing", "transmog", "appearance", "transmogcollection", "mythicplus", "petbattle", "achieveprogress" },
        },
        {
            label = "COMBAT",
            events = { "resurrection", "battlerez", "travelerslog", "craftingorder", "rarespawn" },
        },
        {
            label = "DAILY LIFE",
            events = { "eating", "drinking", "hearthing", "flightpath", "restzone", "idle" },
        },
    }

    local innerW = W - 44
    -- 3 columns at 548px inner width = ~182px each
    local NCOLS = 3
    local colW = math.floor((innerW - 8) / NCOLS)

    for _, group in ipairs(GROUPS) do
        local header = VC.UI:CreateSectionLabel(content, group.label)
        header:SetPoint("TOPLEFT", 0, y)
        header:SetWidth(innerW - 90)
        y = y - 4

        local allBtn = VC.UI:CreateButton(content, "All", 40, 16, function()
            for _, et in ipairs(group.events) do
                VC.Store:Dispatch("SET_EVENT_TOGGLE", { eventType = et, enabled = true })
            end
        end)
        allBtn:SetPoint("TOPRIGHT", content, "TOPRIGHT", -50, y + 4)

        local noneBtn = VC.UI:CreateButton(content, "None", 40, 16, function()
            for _, et in ipairs(group.events) do
                VC.Store:Dispatch("SET_EVENT_TOGGLE", { eventType = et, enabled = false })
            end
        end)
        noneBtn:SetPoint("TOPRIGHT", content, "TOPRIGHT", -4, y + 4)

        y = y - 22

        for i, eventType in ipairs(group.events) do
            local label = VC.Constants.EVENT_TYPE_LABELS[eventType] or eventType
            local cb = VC.UI:CreateCheckbox(content, label, function(checked)
                VC.Store:Dispatch("SET_EVENT_TOGGLE", { eventType = eventType, enabled = checked })
            end)
            local col = (i - 1) % NCOLS
            local row = math.floor((i - 1) / NCOLS)
            cb:SetPoint("TOPLEFT", col * colW + 4, y - row * 24)
            self.eventChecks[eventType] = cb
        end

        local groupRows = math.ceil(#group.events / NCOLS)
        y = y - groupRows * 24 - 8
    end

    content:SetHeight(math.max(1, math.abs(y) + 20))
end

-- ============================================================================
-- LOCALE PAGE
-- ============================================================================

function VC.Settings:BuildLocalePage(parent, W, pageH)
    local c = VC.Theme:GetScheme()
    local page = MakePage(self, parent, pageH, "locale")

    local y = -12

    local header = page:CreateFontString(nil, "OVERLAY")
    header:SetPoint("TOPLEFT", 12, y)
    header:SetFont("Fonts\\FRIZQT__.TTF", 13, "")
    header:SetText("Quote Language")
    header:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, 1)
    y = y - 20

    local desc = page:CreateFontString(nil, "OVERLAY")
    desc:SetPoint("TOPLEFT", 12, y)
    desc:SetPoint("TOPRIGHT", -12, y)
    desc:SetFont("Fonts\\ARIALN.TTF", 11, "")
    desc:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    desc:SetJustifyH("LEFT")
    desc:SetWordWrap(true)
    desc:SetText("Choose the language for companion quotes. 'Auto' uses your WoW client language if translations are available.")
    y = y - 32

    local localeOpts = {
        { key = "auto", label = "Auto (" .. GetLocale() .. ")" },
        { key = "enUS", label = "English" },
        { key = "deDE", label = "Deutsch" },
        { key = "ptBR", label = "Portugues (BR)" },
    }
    local localeGroup = VC.UI:CreateRadioGroup(page, localeOpts, function(selKey)
        VC.Store:Dispatch("SET_CONFIG", { key = "localeQuotes", value = selKey })
        if VC.ApplyLocaleQuotes then VC:ApplyLocaleQuotes() end
        self:RefreshLocalePage()
    end)
    localeGroup:SetPoint("TOPLEFT", 12, y)
    self._localeGroup = localeGroup
    y = y - 30

    -- Separator
    local sep = page:CreateTexture(nil, "ARTWORK")
    sep:SetHeight(1)
    sep:SetPoint("TOPLEFT", 8, y); sep:SetPoint("TOPRIGHT", -8, y)
    sep:SetTexture("Interface\\Buttons\\WHITE8x8")
    sep:SetVertexColor(c.border.r, c.border.g, c.border.b, 0.4)
    y = y - 14

    -- Available translations
    local transHeader = page:CreateFontString(nil, "OVERLAY")
    transHeader:SetPoint("TOPLEFT", 12, y)
    transHeader:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    transHeader:SetText("Available Translations")
    transHeader:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, 1)
    y = y - 20

    -- Scurry German status
    local scurryDE = page:CreateFontString(nil, "OVERLAY")
    scurryDE:SetPoint("TOPLEFT", 12, y)
    scurryDE:SetFont("Fonts\\ARIALN.TTF", 12, "")
    scurryDE:SetJustifyH("LEFT")
    self._localeScurryDE = scurryDE
    y = y - 18

    -- Scurry Portuguese status
    local scurryPT = page:CreateFontString(nil, "OVERLAY")
    scurryPT:SetPoint("TOPLEFT", 12, y)
    scurryPT:SetFont("Fonts\\ARIALN.TTF", 12, "")
    scurryPT:SetJustifyH("LEFT")
    self._localeScurryPT = scurryPT
    y = y - 18

    -- Companion coverage summary
    local coverageLabel = page:CreateFontString(nil, "OVERLAY")
    coverageLabel:SetPoint("TOPLEFT", 12, y)
    coverageLabel:SetPoint("TOPRIGHT", -12, y)
    coverageLabel:SetFont("Fonts\\ARIALN.TTF", 11, "")
    coverageLabel:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    coverageLabel:SetJustifyH("LEFT")
    coverageLabel:SetWordWrap(true)
    self._localeCoverage = coverageLabel
    y = y - 30

    -- Review button
    local reviewBtn = VC.UI:CreateButton(page, "Review Translations", 160, 24, function()
        self:ToggleLocaleReview()
    end)
    reviewBtn:SetPoint("TOPLEFT", 12, y)
end

function VC.Settings:RefreshLocalePage()
    local state = VC.Store:GetState()

    if self._localeGroup then
        self._localeGroup:SetValue(state.config.localeQuotes or "auto")
    end

    -- Check available translations
    local hasScurryDE = VC_QuoteDB_deDE and VC_QuoteDB_deDE.scurry and true or false
    local deCount = 0
    if hasScurryDE then
        for _, cat in pairs(VC_QuoteDB_deDE.scurry) do
            if type(cat) == "table" then deCount = deCount + #cat end
        end
    end

    local hasScurryPT = VC_QuoteDB_ptBR and VC_QuoteDB_ptBR.scurry and true or false
    local ptCount = 0
    if hasScurryPT then
        for _, cat in pairs(VC_QuoteDB_ptBR.scurry) do
            if type(cat) == "table" then ptCount = ptCount + #cat end
        end
    end

    if self._localeScurryDE then
        if hasScurryDE then
            self._localeScurryDE:SetText("|cFF33EE33Scurry (Deutsch)|r -- " .. deCount .. " quotes")
        else
            self._localeScurryDE:SetText("|cFFFF6666Scurry (Deutsch)|r -- not available")
        end
    end

    if self._localeScurryPT then
        if hasScurryPT then
            self._localeScurryPT:SetText("|cFF33EE33Scurry (Portugues)|r -- " .. ptCount .. " quotes")
        else
            self._localeScurryPT:SetText("|cFFFF6666Scurry (Portugues)|r -- not available")
        end
    end

    if self._localeCoverage then
        local active = state.config.localeQuotes or "auto"
        local effectiveLocale = active
        if active == "auto" then effectiveLocale = GetLocale() end
        local localeNames = { deDE = "German", ptBR = "Portuguese" }
        local localeAvail = { deDE = hasScurryDE, ptBR = hasScurryPT }
        local localeName = localeNames[effectiveLocale]
        if localeName and localeAvail[effectiveLocale] then
            self._localeCoverage:SetText("Scurry quotes are using " .. localeName .. " translations. Other companions will use English until translations are added.")
        else
            self._localeCoverage:SetText("All companion quotes are using English.")
        end
    end
end

-- ============================================================================
-- LOCALE REVIEW DIALOG
-- ============================================================================

-- Rejection reason categories for translation review
local REVIEW_REASONS = {
    { key = "voice",    label = "Voice",   tip = "Voice drift -- lost Scurry's personality" },
    { key = "meaning",  label = "Meaning", tip = "Meaning shift -- joke or reference lost" },
    { key = "phrasing", label = "Phrasing",tip = "Unnatural phrasing -- reads like machine translation" },
    { key = "cultural", label = "Cultural",tip = "Broken cultural references" },
    { key = "tone",     label = "Tone",    tip = "Tonal inconsistency -- quality tier mismatch" },
    { key = "rewrite",  label = "Rewrite", tip = "Needs complete rewrite -- does not work" },
}

local function isQuoteRejected(reviews, qid)
    local r = reviews[qid]
    if r == false then return true end -- legacy format
    if type(r) == "table" then
        for _ in pairs(r) do return true end
    end
    return false
end

local function getReasonLabels(reviews, qid)
    local r = reviews[qid]
    if r == false then return "legacy" end
    if type(r) ~= "table" then return "" end
    local out = {}
    for _, reason in ipairs(REVIEW_REASONS) do
        if r[reason.key] then out[#out + 1] = reason.label end
    end
    return table.concat(out, ", ")
end

function VC.Settings:ToggleLocaleReview()
    if self._reviewFrame then
        self._reviewFrame:SetShown(not self._reviewFrame:IsShown())
        if self._reviewFrame:IsShown() then self:RefreshReviewList() end
        return
    end
    self:BuildLocaleReview()
end

function VC.Settings:BuildLocaleReview()
    local c = VC.Theme:GetScheme()
    local W, H = 800, 600

    local f = CreateFrame("Frame", "VC_LocaleReview", UIParent, "BackdropTemplate")
    f:SetSize(W, H)
    f:SetPoint("CENTER")
    f:SetFrameStrata("DIALOG")
    f:SetMovable(true)
    f:SetClampedToScreen(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)
    f:SetBackdrop(VC.Theme.BACKDROP_PANEL)
    f:SetBackdropColor(c.bg.r, c.bg.g, c.bg.b, c.bg.a)
    f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 1)
    tinsert(UISpecialFrames, "VC_LocaleReview")
    self._reviewFrame = f

    -- Title
    local title = f:CreateFontString(nil, "OVERLAY")
    title:SetPoint("TOPLEFT", 10, -8)
    title:SetFont("Fonts\\MORPHEUS.ttf", 14, "")
    title:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, 1)
    self._reviewTitle = title

    -- Stats
    local stats = f:CreateFontString(nil, "OVERLAY")
    stats:SetPoint("TOP", 0, -8)
    stats:SetFont("Fonts\\ARIALN.TTF", 11, "")
    stats:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    self._reviewStats = stats

    -- Close button
    local closeBtn = CreateFrame("Button", nil, f)
    closeBtn:SetSize(16, 16)
    closeBtn:SetPoint("TOPRIGHT", -6, -8)
    local closeIcon = closeBtn:CreateTexture(nil, "OVERLAY")
    closeIcon:SetAllPoints()
    closeIcon:SetAtlas("XMarksTheSpot")
    closeIcon:SetVertexColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    closeBtn:SetScript("OnClick", function() f:Hide() end)

    -- Locale selector (only if multiple locales available)
    self._reviewLocale = self._reviewLocale or "deDE"
    local localeY = -28
    local availLocales = {}
    if VC_QuoteDB_deDE and VC_QuoteDB_deDE.scurry then availLocales[#availLocales + 1] = { key = "deDE", label = "Deutsch" } end
    if VC_QuoteDB_ptBR and VC_QuoteDB_ptBR.scurry then availLocales[#availLocales + 1] = { key = "ptBR", label = "Portugues (BR)" } end
    if #availLocales > 1 then
        local locGrp = VC.UI:CreateRadioGroup(f, availLocales, function(key)
            self._reviewLocale = key
            self._reviewTitle:SetText("Translation Review -- Scurry (" .. key .. ")")
            self:BuildReviewQuoteList()
            self:RefreshReviewList()
        end)
        locGrp:SetPoint("TOPLEFT", 10, localeY)
        locGrp:SetValue(self._reviewLocale)
        localeY = localeY - 24
    elseif #availLocales == 1 then
        self._reviewLocale = availLocales[1].key
    end
    self._reviewTitle:SetText("Translation Review -- Scurry (" .. self._reviewLocale .. ")")

    -- Filter buttons
    local filterY = localeY
    local filters = { { key = "all", label = "All" }, { key = "unreviewed", label = "Unreviewed" }, { key = "approved", label = "Approved" }, { key = "rejected", label = "Rejected" } }
    self._reviewFilter = "all"
    local filterGroup = VC.UI:CreateRadioGroup(f, filters, function(key)
        self._reviewFilter = key
        self:RefreshReviewList()
    end)
    filterGroup:SetPoint("TOPLEFT", 10, filterY)
    self._reviewFilterGroup = filterGroup

    -- Export button
    local exportBtn = VC.UI:CreateButton(f, "Export Rejected", 120, 22, function()
        self:ExportLocaleReview()
    end)
    exportBtn:SetPoint("TOPRIGHT", -8, filterY)

    -- Scroll frame
    local scrollTop = filterY - 26
    local scroll = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", 8, scrollTop)
    scroll:SetPoint("BOTTOMRIGHT", -28, 8)
    VC.UI.StyleScrollBar(scroll)

    local content = CreateFrame("Frame", nil, scroll)
    content:SetSize(W - 40, 1)
    scroll:SetScrollChild(content)
    self._reviewContent = content
    self._reviewScroll = scroll

    scroll:EnableMouseWheel(true)
    scroll:SetScript("OnMouseWheel", function(sf, delta)
        local cur = sf:GetVerticalScroll()
        local maxS = math.max(0, content:GetHeight() - sf:GetHeight())
        sf:SetVerticalScroll(math.max(0, math.min(maxS, cur - delta * 30)))
    end)

    -- Virtual scroll row pool (fixed count, recycled on scroll)
    self._reviewRows = {}
    self._reviewROW_H = 80
    self._reviewVISIBLE = 10
    local ROW_W = W - 44

    for i = 1, self._reviewVISIBLE do
        local row = CreateFrame("Frame", nil, content, "BackdropTemplate")
        row:SetSize(ROW_W, self._reviewROW_H)
        row:SetBackdrop(VC.Theme.BACKDROP_INSET)

        -- ID + category
        row._idText = row:CreateFontString(nil, "OVERLAY")
        row._idText:SetPoint("TOPLEFT", 6, -6)
        row._idText:SetFont("Fonts\\ARIALN.TTF", 12, "")
        row._idText:SetTextColor(0.8, 0.8, 0.8, 1)

        -- Locale text (translated quote)
        row._deText = row:CreateFontString(nil, "OVERLAY")
        row._deText:SetPoint("TOPLEFT", 80, -4)
        row._deText:SetPoint("TOPRIGHT", -32, -4)
        row._deText:SetFont("Fonts\\ARIALN.TTF", 13, "")
        row._deText:SetTextColor(0.93, 0.93, 0.93, 1)
        row._deText:SetJustifyH("LEFT")
        row._deText:SetWordWrap(true)
        row._deText:SetMaxLines(2)

        -- English text (below, lighter)
        row._enText = row:CreateFontString(nil, "OVERLAY")
        row._enText:SetPoint("TOPLEFT", 80, -34)
        row._enText:SetPoint("TOPRIGHT", -32, -34)
        row._enText:SetFont("Fonts\\ARIALN.TTF", 12, "")
        row._enText:SetTextColor(0.85, 0.85, 0.85, 1)
        row._enText:SetJustifyH("LEFT")
        row._enText:SetMaxLines(1)

        -- Rejection reason toggle buttons (bottom row)
        row._reasonBtns = {}
        for ri, reason in ipairs(REVIEW_REASONS) do
            local rb = CreateFrame("Button", nil, row, "BackdropTemplate")
            rb:SetSize(68, 16)
            rb:SetPoint("BOTTOMLEFT", 80 + (ri - 1) * 72, 4)
            rb:SetBackdrop(VC.Theme.BACKDROP_INSET)
            rb:SetBackdropColor(0.15, 0.15, 0.18, 0.6)
            rb:SetBackdropBorderColor(0.3, 0.3, 0.3, 0.4)
            local rl = rb:CreateFontString(nil, "OVERLAY")
            rl:SetPoint("CENTER")
            rl:SetFont("Fonts\\ARIALN.TTF", 10, "")
            rl:SetTextColor(0.6, 0.6, 0.6, 1)
            rl:SetText(reason.label)
            rb._label = rl
            rb._key = reason.key
            rb._tip = reason.tip
            rb:SetScript("OnEnter", function(btn)
                GameTooltip:SetOwner(btn, "ANCHOR_TOP")
                GameTooltip:SetText(reason.tip, 1, 1, 1, 1, true)
                GameTooltip:Show()
            end)
            rb:SetScript("OnLeave", GameTooltip_Hide)
            row._reasonBtns[ri] = rb
        end

        -- Approve button (green tick, right side bar)
        local okBtn = CreateFrame("Button", nil, row, "BackdropTemplate")
        okBtn:SetSize(28, self._reviewROW_H)
        okBtn:SetPoint("TOPRIGHT", 0, 0)
        okBtn:SetBackdrop(VC.Theme.BACKDROP_INSET)
        okBtn:SetBackdropColor(0.1, 0.2, 0.1, 0.6)
        okBtn:SetBackdropBorderColor(0.2, 0.4, 0.2, 0.4)
        local okLabel = okBtn:CreateFontString(nil, "OVERLAY")
        okLabel:SetPoint("CENTER")
        okLabel:SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE")
        okLabel:SetText("|TInterface\\RaidFrame\\ReadyCheck-Ready:14|t")
        okBtn._label = okLabel
        okBtn:SetScript("OnEnter", function(btn)
            btn:SetBackdropColor(0.15, 0.35, 0.15, 0.8)
            GameTooltip:SetOwner(btn, "ANCHOR_LEFT")
            GameTooltip:SetText("Mark as reviewed / OK", 1, 1, 1, 1, true)
            GameTooltip:Show()
        end)
        okBtn:SetScript("OnLeave", function(btn)
            btn:SetBackdropColor(0.1, 0.2, 0.1, 0.6)
            GameTooltip_Hide()
        end)
        row._okBtn = okBtn

        row:Hide()
        self._reviewRows[i] = row
    end

    -- Override scroll to virtual-update rows
    scroll:SetScript("OnMouseWheel", function(sf, delta)
        local cur = sf:GetVerticalScroll()
        local maxS = math.max(0, content:GetHeight() - sf:GetHeight())
        sf:SetVerticalScroll(math.max(0, math.min(maxS, cur - delta * self._reviewROW_H * 2)))
        self:UpdateReviewRows()
    end)
    scroll:SetScript("OnVerticalScroll", function() self:UpdateReviewRows() end)

    -- Build flat quote list
    self:BuildReviewQuoteList()
    self._reviewFilterGroup:SetValue("all")
    self:RefreshReviewList()
    f:Show()
end

function VC.Settings:BuildReviewQuoteList()
    self._reviewQuotes = {}
    local enDB = (VC._enQuoteBackup and VC._enQuoteBackup.scurry) or (VC_QuoteDB and VC_QuoteDB.scurry)
    local localeDBs = { deDE = VC_QuoteDB_deDE, ptBR = VC_QuoteDB_ptBR }
    local locDB = localeDBs[self._reviewLocale or "deDE"]
    local localeQuotes = locDB and locDB.scurry
    if not localeQuotes then return end

    for cat, quotes in pairs(localeQuotes) do
        if type(quotes) == "table" then
            for _, q in ipairs(quotes) do
                local enText = ""
                if enDB and enDB[cat] then
                    for _, eq in ipairs(enDB[cat]) do
                        if eq.id == q.id then enText = eq.text; break end
                    end
                end
                self._reviewQuotes[#self._reviewQuotes + 1] = {
                    id = q.id, cat = cat, de = q.text, en = enText, quality = q.quality
                }
            end
        end
    end
    table.sort(self._reviewQuotes, function(a, b) return a.id < b.id end)
end

function VC.Settings:RefreshReviewList()
    if not self._reviewContent then return end
    VC_DB = VC_DB or {}
    VC_DB.localeReviews = VC_DB.localeReviews or {}
    local reviews = VC_DB.localeReviews

    -- Filter
    self._reviewFiltered = {}
    for _, q in ipairs(self._reviewQuotes or {}) do
        local rej = isQuoteRejected(reviews, q.id)
        local approved = reviews[q.id] == "ok"
        local unreviewed = reviews[q.id] == nil
        if self._reviewFilter == "all" then
            self._reviewFiltered[#self._reviewFiltered + 1] = q
        elseif self._reviewFilter == "unreviewed" and unreviewed then
            self._reviewFiltered[#self._reviewFiltered + 1] = q
        elseif self._reviewFilter == "approved" and approved then
            self._reviewFiltered[#self._reviewFiltered + 1] = q
        elseif self._reviewFilter == "rejected" and rej then
            self._reviewFiltered[#self._reviewFiltered + 1] = q
        end
    end

    -- Stats
    local total = #(self._reviewQuotes or {})
    local rejCount, okCount = 0, 0
    for _, q in ipairs(self._reviewQuotes or {}) do
        if isQuoteRejected(reviews, q.id) then rejCount = rejCount + 1
        elseif reviews[q.id] == "ok" then okCount = okCount + 1 end
    end
    if self._reviewStats then
        self._reviewStats:SetText(total .. " total | " .. okCount .. " ok | " .. rejCount .. " rejected")
    end

    -- Set content height for scrollbar, reset scroll position
    local ROW_H = self._reviewROW_H
    self._reviewContent:SetHeight(math.max(1, #self._reviewFiltered * (ROW_H + 2)))
    if self._reviewScroll then self._reviewScroll:SetVerticalScroll(0) end
    self:UpdateReviewRows()
end

function VC.Settings:UpdateReviewRows()
    if not self._reviewScroll or not self._reviewFiltered then return end
    local c = VC.Theme:GetScheme()
    local reviews = VC_DB and VC_DB.localeReviews or {}
    local ROW_H = self._reviewROW_H
    local scrollOffset = self._reviewScroll:GetVerticalScroll()
    local firstVisible = math.floor(scrollOffset / (ROW_H + 2)) + 1
    local filtered = self._reviewFiltered

    for i, row in ipairs(self._reviewRows) do
        local dataIdx = firstVisible + i - 1
        local q = filtered[dataIdx]
        if q then
            row:ClearAllPoints()
            row:SetPoint("TOPLEFT", 0, -((dataIdx - 1) * (ROW_H + 2)))
            row:Show()

            row._idText:SetText(q.id .. "\n" .. q.cat)
            row._deText:SetText(q.de)
            row._deText:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, 1)
            row._enText:SetText("EN: " .. q.en)

            local rej = isQuoteRejected(reviews, q.id)
            local approved = reviews[q.id] == "ok"
            if rej then
                row:SetBackdropColor(0.3, 0.1, 0.1, 0.5)
                row:SetBackdropBorderColor(0.7, 0.2, 0.2, 0.6)
            elseif approved then
                row:SetBackdropColor(0.1, 0.2, 0.1, 0.4)
                row:SetBackdropBorderColor(0.2, 0.5, 0.2, 0.5)
            else
                row:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, 1)
                row:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 1)
            end

            -- Update reason buttons
            local qid = q.id
            local qReasons = (type(reviews[qid]) == "table") and reviews[qid] or {}
            for _, rb in ipairs(row._reasonBtns) do
                local rKey = rb._key
                local active = qReasons[rKey]
                if active then
                    rb:SetBackdropColor(0.5, 0.15, 0.15, 0.8)
                    rb:SetBackdropBorderColor(0.7, 0.25, 0.25, 0.8)
                    rb._label:SetTextColor(1, 0.6, 0.6, 1)
                else
                    rb:SetBackdropColor(0.15, 0.15, 0.18, 0.6)
                    rb:SetBackdropBorderColor(0.3, 0.3, 0.3, 0.4)
                    rb._label:SetTextColor(0.6, 0.6, 0.6, 1)
                end
                rb:SetScript("OnClick", function()
                    VC_DB.localeReviews = VC_DB.localeReviews or {}
                    local cur = VC_DB.localeReviews[qid]
                    -- Migrate legacy format
                    if cur == false then cur = {} end
                    if type(cur) ~= "table" then cur = {} end
                    if cur[rKey] then
                        cur[rKey] = nil
                    else
                        cur[rKey] = true
                    end
                    -- If no reasons left, clear the entry
                    local hasAny = false
                    for _ in pairs(cur) do hasAny = true; break end
                    if hasAny then
                        VC_DB.localeReviews[qid] = cur
                    else
                        VC_DB.localeReviews[qid] = nil
                    end
                    self:RefreshReviewList()
                end)
            end

            -- Approve button state
            if approved then
                row._okBtn:SetBackdropColor(0.1, 0.4, 0.1, 0.8)
                row._okBtn:SetBackdropBorderColor(0.2, 0.6, 0.2, 0.8)
            else
                row._okBtn:SetBackdropColor(0.1, 0.2, 0.1, 0.6)
                row._okBtn:SetBackdropBorderColor(0.2, 0.4, 0.2, 0.4)
            end
            row._okBtn:SetScript("OnClick", function()
                VC_DB.localeReviews = VC_DB.localeReviews or {}
                if VC_DB.localeReviews[qid] == "ok" then
                    VC_DB.localeReviews[qid] = nil -- un-approve
                else
                    VC_DB.localeReviews[qid] = "ok" -- approve (clears any rejections)
                end
                self:RefreshReviewList()
            end)
        else
            row:Hide()
        end
    end
end

function VC.Settings:ExportLocaleReview()
    VC_DB = VC_DB or {}
    local reviews = VC_DB.localeReviews or {}

    local lines = {}
    lines[#lines + 1] = "Vamoose's Companion - Translation Review (" .. (self._reviewLocale or "deDE") .. " - Scurry)"
    lines[#lines + 1] = "Date: " .. date("%Y-%m-%d %H:%M")
    lines[#lines + 1] = "Reviewer: " .. (UnitName("player") or "Unknown")
    lines[#lines + 1] = ""

    local rejected = {}
    for _, q in ipairs(self._reviewQuotes or {}) do
        if isQuoteRejected(reviews, q.id) then
            rejected[#rejected + 1] = q
        end
    end

    lines[#lines + 1] = "Rejected: " .. #rejected .. " quotes"
    lines[#lines + 1] = ""

    local loc = self._reviewLocale or "deDE"
    for _, q in ipairs(rejected) do
        local reasons = getReasonLabels(reviews, q.id)
        lines[#lines + 1] = "[" .. q.id .. "] " .. q.cat .. (reasons ~= "" and (" -- " .. reasons) or "")
        lines[#lines + 1] = "EN: " .. q.en
        lines[#lines + 1] = loc .. ": " .. q.de
        lines[#lines + 1] = "---"
    end

    -- Reuse Model Viewer export dialog pattern
    if VC.ModelViewer and VC.ModelViewer.ShowExportDialog then
        VC.ModelViewer:ShowExportDialog(table.concat(lines, "\n"))
    else
        -- Fallback: simple copy dialog
        local text = table.concat(lines, "\n")
        local d = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
        d:SetSize(600, 400)
        d:SetPoint("CENTER")
        d:SetFrameStrata("DIALOG")
        d:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 })
        d:SetBackdropColor(0.05, 0.05, 0.08, 0.95)
        d:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
        local sf = CreateFrame("ScrollFrame", nil, d, "UIPanelScrollFrameTemplate")
        sf:SetPoint("TOPLEFT", 6, -6); sf:SetPoint("BOTTOMRIGHT", -28, 6)
        local eb = CreateFrame("EditBox", nil, sf)
        eb:SetMultiLine(true); eb:SetAutoFocus(false)
        eb:SetFont("Fonts\\ARIALN.TTF", 11, ""); eb:SetTextColor(0.9, 0.9, 0.9, 1)
        eb:SetWidth(560)
        eb:SetScript("OnEscapePressed", function(e) e:ClearFocus(); d:Hide() end)
        sf:SetScrollChild(eb)
        eb:SetText(text); eb:HighlightText(); eb:SetFocus()
        d:Show()
    end
end

-- ============================================================================
-- HISTORY PAGE (wider: quote text wraps at ~420px)
-- ============================================================================

function VC.Settings:BuildHistoryPage(parent, W, pageH)
    local c = VC.Theme:GetScheme()
    local page = MakePage(self, parent, pageH, "history")

    local filterBarH = 30
    local filterBar = CreateFrame("Frame", nil, page)
    filterBar:SetPoint("TOPLEFT", 4, -4); filterBar:SetPoint("TOPRIGHT", -4, -4)
    filterBar:SetHeight(filterBarH)
    self.historyFilter = "all"
    self.historyFilterBtns = {}

    local fkeys = { "all" }
    for k in pairs(VC_CompanionProfiles) do
        if VC_QuoteDB and VC_QuoteDB[k] then fkeys[#fkeys + 1] = k end
    end
    table.sort(fkeys, function(a, b) return (a == "all") or (b ~= "all" and a < b) end)

    local fx = 0
    for _, fk in ipairs(fkeys) do
        local lbl = (fk == "all") and "All" or (VC_CompanionProfiles[fk] and VC_CompanionProfiles[fk].name or fk)
        local fw = math.max(36, math.floor(#lbl * 7 + 16))
        local fbtn = VC.UI:CreatePillButton(filterBar, lbl, fw, 22, function()
            self.historyFilter = fk
            for k2, b in pairs(self.historyFilterBtns) do
                VC.UI:SetPillActive(b, k2 == fk)
            end
            self:RefreshHistory()
        end)
        fbtn:SetPoint("LEFT", fx, 0)
        self.historyFilterBtns[fk] = fbtn
        fx = fx + fw + 4
    end
    VC.UI:SetPillActive(self.historyFilterBtns["all"], true)

    local filterSep = page:CreateTexture(nil, "ARTWORK")
    filterSep:SetHeight(1)
    filterSep:SetPoint("TOPLEFT", 4, -(filterBarH + 6)); filterSep:SetPoint("TOPRIGHT", -4, -(filterBarH + 6))
    filterSep:SetTexture(WHITE); filterSep:SetVertexColor(c.border.r, c.border.g, c.border.b, 0.4)
    filterSep._alpha = 0.4; VC.Theme:Register(filterSep, "Separator")

    local histScrollH = pageH - filterBarH - 8 - 24
    local histScrollW = W - 8
    local histScroll, histContent = VC.UI:CreateScrollFrame(page, histScrollW, histScrollH)
    histScroll:SetPoint("TOPLEFT", 4, -(filterBarH + 8))
    VC.UI.StyleScrollBar(histScroll)
    self.historyContent = histContent
    self.historyRows = {}

    -- Clear button (dim text, bottom right)
    local clearBtn = page:CreateFontString(nil, "OVERLAY")
    clearBtn:SetPoint("BOTTOMRIGHT", -8, 4)
    clearBtn:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    clearBtn:SetText("Clear History")
    clearBtn:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    VC.Theme:Register(clearBtn, "DimText")

    local clearClickArea = CreateFrame("Button", nil, page)
    clearClickArea:SetPoint("BOTTOMRIGHT", -8, 2)
    clearClickArea:SetSize(90, 18)
    clearClickArea:SetScript("OnClick", function()
        VC.Store:Dispatch("CLEAR_HISTORY")
        if VC.Engine and VC.Engine.RebuildHistorySet then VC.Engine:RebuildHistorySet() end
        self:RefreshHistory()
    end)
    clearClickArea:SetScript("OnEnter", function() local s = VC.Theme:GetScheme(); clearBtn:SetTextColor(s.text_header.r, s.text_header.g, s.text_header.b, 1) end)
    clearClickArea:SetScript("OnLeave", function() local s = VC.Theme:GetScheme(); clearBtn:SetTextColor(s.text_dim.r, s.text_dim.g, s.text_dim.b, 1) end)
end

-- ============================================================================
-- REFRESH (sync all controls to current state)
-- ============================================================================
-- INFO PAGE (discord, support, about)
-- ============================================================================

function VC.Settings:BuildInfoPage(parent, W, pageH)
    local c = VC.Theme:GetScheme()
    local page = MakePage(self, parent, pageH, "info")

    local y = -12

    -- Discord link
    local discordRow = CreateFrame("Frame", nil, page)
    discordRow:SetHeight(24)
    discordRow:SetPoint("TOPLEFT", 12, y)
    discordRow:SetPoint("TOPRIGHT", -12, y)

    local discordIcon = discordRow:CreateTexture(nil, "ARTWORK")
    discordIcon:SetPoint("LEFT", 0, 0)
    discordIcon:SetSize(20, 20)
    discordIcon:SetAtlas("UI-ChatIcon-Discord")

    local discordLabel = discordRow:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    discordLabel:SetPoint("LEFT", discordIcon, "RIGHT", 6, 0)
    discordLabel:SetText("Discord")

    local discordBox = CreateFrame("EditBox", nil, discordRow)
    discordBox:SetPoint("LEFT", discordLabel, "RIGHT", 8, 0)
    discordBox:SetPoint("RIGHT", 0, 0)
    discordBox:SetHeight(20)
    discordBox:SetAutoFocus(false)
    discordBox:SetFontObject(GameFontHighlightSmall)
    discordBox:SetText("https://discord.gg/RWZaxJaHFP")
    discordBox:SetCursorPosition(0)
    discordBox:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    VC.Theme:Register(discordBox, "DimText")
    discordBox:SetScript("OnEditFocusGained", function(self) self:HighlightText() end)
    discordBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    discordBox:SetScript("OnTextChanged", function(self, userInput) if userInput then self:SetText("https://discord.gg/RWZaxJaHFP"); self:SetCursorPosition(0) end end)
    y = y - 26

    -- Buy Me a Coffee link
    local bmacRow = CreateFrame("Frame", nil, page)
    bmacRow:SetHeight(24)
    bmacRow:SetPoint("TOPLEFT", 12, y)
    bmacRow:SetPoint("TOPRIGHT", -12, y)

    local bmacIcon = bmacRow:CreateTexture(nil, "ARTWORK")
    bmacIcon:SetPoint("LEFT", 0, 0)
    bmacIcon:SetSize(20, 20)
    bmacIcon:SetAtlas("auctionhouse-icon-coin-default")

    local bmacLabel = bmacRow:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    bmacLabel:SetPoint("LEFT", bmacIcon, "RIGHT", 6, 0)
    bmacLabel:SetText("Support")

    local bmacBox = CreateFrame("EditBox", nil, bmacRow)
    bmacBox:SetPoint("LEFT", bmacLabel, "RIGHT", 8, 0)
    bmacBox:SetPoint("RIGHT", 0, 0)
    bmacBox:SetHeight(20)
    bmacBox:SetAutoFocus(false)
    bmacBox:SetFontObject(GameFontHighlightSmall)
    bmacBox:SetText("https://buymeacoffee.com/Vamoose")
    bmacBox:SetCursorPosition(0)
    bmacBox:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    VC.Theme:Register(bmacBox, "DimText")
    bmacBox:SetScript("OnEditFocusGained", function(self) self:HighlightText() end)
    bmacBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    bmacBox:SetScript("OnTextChanged", function(self, userInput) if userInput then self:SetText("https://buymeacoffee.com/Vamoose"); self:SetCursorPosition(0) end end)
    y = y - 30

    AddSep(page, y)
    y = y - 14

    -- About text
    local about = page:CreateFontString(nil, "OVERLAY")
    about:SetPoint("TOPLEFT", 12, y)
    about:SetPoint("TOPRIGHT", -12, y)
    about:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    about:SetJustifyH("LEFT")
    about:SetWordWrap(true)
    about:SetSpacing(4)
    about:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    VC.Theme:Register(about, "DimText")
    about:SetText(
        "Vamoose's Companion gives you a friend who pays attention while you play. " ..
        "They react to what you do - boss kills, achievements, fishing, crafting, exploring new zones, and dozens more moments. " ..
        "Each companion has their own personality and hundreds of hand-written quotes.\n\n" ..
        "Pick a companion, adjust the chattiness, and just play. They'll take it from there."
    )
end

-- ============================================================================

function VC.Settings:Refresh()
    if not self.frame then return end
    local state = VC.Store:GetState()
    local c = VC.Theme:GetScheme()
    local activeKey = state.config.activeCompanion

    -- Ensure editing key is set (default to active companion)
    if not self._editingKey then
        self._editingKey = activeKey
    end

    -- Sidebar background tint
    if self._sidebarBg then
        self._sidebarBg:SetVertexColor(c.panel.r, c.panel.g, c.panel.b, 0.6)
    end

    -- Update sidebar active dot + bond fills + mood indicators
    self:RefreshSidebar(activeKey)

    -- Update right panel companion content for editing key
    self:RefreshCompanionPage(self._editingKey)

    -- Display page
    self:RefreshDisplayPage()

    -- Event toggles
    for eventType, cb in pairs(self.eventChecks or {}) do
        cb.checkbox:SetChecked(state.config.eventToggles[eventType] ~= false)
    end

    -- History (only if visible)
    if self.activeTab == "history" then self:RefreshHistory() end
end

function VC.Settings:RefreshSidebar(activeKey)
    if not self._sidebarRows then return end
    local c = VC.Theme:GetScheme()

    for key, row in pairs(self._sidebarRows) do
        -- Active companion dot
        if row._activeDot then row._activeDot:SetShown(key == activeKey) end
        -- Preview border (theme-aware, preserve companion border color)
        if row._previewFrame then
            local tc = row._themeColor or c.border
            local alpha = row._editing and 0.9 or 0.5
            row._previewFrame:SetBackdropColor(c.bg.r, c.bg.g, c.bg.b, 1)
            row._previewFrame:SetBackdropBorderColor(tc.r, tc.g, tc.b, alpha)
        end

        -- Bond fill
        if row._bondFill and VC.Bonding then
            local info = VC.Bonding:GetLevelInfo(key)
            if info then
                local pct = 0
                if info.nextThreshold and info.nextThreshold > 0 then
                    local levels = VC.Constants.BONDING.LEVELS
                    local prevThresh = 0
                    for _, lvl in ipairs(levels) do
                        if lvl.name == info.name then break end
                        prevThresh = lvl.threshold
                    end
                    local span = info.nextThreshold - prevThresh
                    if span > 0 then pct = math.max(0, math.min(1, (info.rapport - prevThresh) / span)) end
                else
                    pct = 1
                end
                local fw = math.max(1, math.floor((row._bondFillMaxW or 110) * pct))
                row._bondFill:SetWidth(fw)
            end
        end

        -- Mood dot + name
        if row._moodDot and VC.Mood then
            local mood = VC.Mood:GetMood()
            local mc = VC.Constants.MOOD_COLORS[mood] or VC.Constants.MOOD_COLORS.NEUTRAL
            row._moodDot:SetVertexColor(mc.r, mc.g, mc.b, 1)
            if row._moodName then
                local moodStr = mood:sub(1,1) .. mood:sub(2):lower()
                row._moodName:SetText(moodStr)
                row._moodName:SetTextColor(mc.r, mc.g, mc.b, 1)
            end
        end
    end
end

-- ============================================================================
-- HISTORY REFRESH
-- ============================================================================

function VC.Settings:RefreshHistory()
    if not self.historyContent then return end
    local state = VC.Store:GetState()
    local history = state.quoteHistory
    local count = #history
    local c = VC.Theme:GetScheme()
    local filterKey = self.historyFilter or "all"
    local W = VC.Constants.SETTINGS.WIDTH - VC.Constants.SETTINGS.SIDEBAR_W

    for _, row in ipairs(self.historyRows) do
        row:Hide()
    end

    local rowH = 44
    local rowW = W - 36
    local y = 0
    local visIdx = 0

    for i = count, math.max(1, count - 99), -1 do
        local entry = history[i]
        if type(entry) ~= "table" then break end

        if filterKey == "all" or entry.companionKey == filterKey then
            visIdx = visIdx + 1
            local row = self.historyRows[visIdx]
            if not row then
                row = CreateFrame("Button", nil, self.historyContent)
                row:SetSize(rowW, rowH)
                row:EnableMouse(true)

                local rowBg = row:CreateTexture(nil, "BACKGROUND")
                rowBg:SetAllPoints(); rowBg:SetColorTexture(1, 1, 1, 0)
                row.bg = rowBg

                row:SetScript("OnEnter", function(self)
                    self.bg:SetColorTexture(c.panel_alt and c.panel_alt.r or 0.067,
                                           c.panel_alt and c.panel_alt.g or 0.122,
                                           c.panel_alt and c.panel_alt.b or 0.220, 0.5)
                end)
                row:SetScript("OnLeave", function(self) self.bg:SetColorTexture(1, 1, 1, 0) end)

                local rowSep = row:CreateTexture(nil, "ARTWORK")
                rowSep:SetHeight(1)
                rowSep:SetPoint("BOTTOMLEFT", 0, 0); rowSep:SetPoint("BOTTOMRIGHT", 0, 0)
                rowSep:SetColorTexture(c.border.r, c.border.g, c.border.b, 0.4)

                local dot = row:CreateTexture(nil, "ARTWORK")
                dot:SetSize(8, 8); dot:SetPoint("TOPLEFT", 2, -8)
                dot:SetTexture(WHITE)
                row.dot = dot

                -- Wider quote text: fills from LEFT+16 to RIGHT-36 (more room at 580px)
                local qt = row:CreateFontString(nil, "OVERLAY")
                qt:SetPoint("TOPLEFT", 16, -4)
                qt:SetPoint("RIGHT", row, "RIGHT", -36, 0)
                qt:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
                qt:SetJustifyH("LEFT"); qt:SetMaxLines(1)
                qt:SetShadowOffset(1, -1); qt:SetShadowColor(0, 0, 0, 0.8)
                row.quoteText = qt

                local meta = row:CreateFontString(nil, "OVERLAY")
                meta:SetPoint("TOPLEFT", 16, -20)
                meta:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
                meta:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
                row.metaText = meta

                local replayBtn = CreateFrame("Button", nil, row)
                replayBtn:SetSize(28, 20); replayBtn:SetPoint("RIGHT", row, "RIGHT", -4, 4)
                local replayLabel = replayBtn:CreateFontString(nil, "OVERLAY")
                replayLabel:SetAllPoints(); replayLabel:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
                replayLabel:SetText(">"); replayLabel:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
                replayBtn:SetScript("OnEnter", function() replayLabel:SetTextColor(c.accent.r, c.accent.g, c.accent.b, 1) end)
                replayBtn:SetScript("OnLeave", function() replayLabel:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1) end)
                row.replayBtn = replayBtn

                self.historyRows[visIdx] = row
            end

            row:SetPoint("TOPLEFT", 0, y)
            row:Show()

            local qc = VC.Constants.QUALITY_COLORS[entry.quality] or VC.Constants.QUALITY_COLORS.common
            row.dot:SetColorTexture(qc.r, qc.g, qc.b, 1)

            local text = entry.text or ""
            if #text > 100 then text = text:sub(1, 97) .. "..." end
            row.quoteText:SetText('"' .. text .. '"')
            row.quoteText:SetTextColor(qc.r, qc.g, qc.b)

            local companionName = ""
            local cp = VC_CompanionProfiles and VC_CompanionProfiles[entry.companionKey]
            if cp then companionName = cp.name end
            local eventLabel = VC.Constants.EVENT_TYPE_LABELS[entry.eventType] or entry.eventType or ""
            local timeAgo = ""
            if entry.timestamp then
                local delta = math.max(0, GetTime() - entry.timestamp)
                if delta < 60 then
                    timeAgo = "just now"
                elseif delta < 3600 then
                    timeAgo = math.floor(delta / 60) .. "m ago"
                else
                    timeAgo = math.floor(delta / 3600) .. "h ago"
                end
            end
            row.metaText:SetText(companionName .. " / " .. eventLabel .. " / " .. timeAgo)

            local replayEntry = entry
            row.replayBtn:SetScript("OnClick", function()
                local companion = VC_CompanionProfiles and VC_CompanionProfiles[replayEntry.companionKey]
                if companion then
                    VC.EventBus:Trigger("VC_QUOTE_READY", {
                        quote = { id = replayEntry.id, text = replayEntry.text, quality = replayEntry.quality, tags = {} },
                        companion = companion,
                        event = { eventType = replayEntry.eventType, context = {} },
                    })
                end
            end)

            y = y - rowH
        end
    end

    if self.historyContent then
        self.historyContent:SetHeight(math.max(1, math.abs(y)))
    end
end
