local AN = _G.AmmeNotepad
if not AN then return end

local function clamp(v, lo, hi) if v < lo then return lo elseif v > hi then return hi else return v end end
local function trim(s) s = tostring(s or ""); return (s:gsub("^%s+", ""):gsub("%s+$", "")) end
local function trunc(s, n) s = tostring(s or ""); if #s <= n then return s end; return s:sub(1, math.max(1, n - 3)) .. "..." end
local function truncToWidth(fs, s, maxW)
  s = tostring(s or "")
  maxW = tonumber(maxW) or 0
  if not fs or maxW <= 0 then
    return ""
  end

  fs:SetText(s)
  if (fs:GetStringWidth() or 0) <= maxW then
    return s
  end

  local ellipsis = "..."
  fs:SetText(ellipsis)
  if (fs:GetStringWidth() or 0) > maxW then
    return ""
  end

  local lo, hi = 0, #s
  while lo < hi do
    local mid = math.floor((lo + hi + 1) / 2)
    local candidate = s:sub(1, mid) .. ellipsis
    fs:SetText(candidate)
    if (fs:GetStringWidth() or 0) <= maxW then
      lo = mid
    else
      hi = mid - 1
    end
  end
  return s:sub(1, lo) .. ellipsis
end

local ICON = {
  save = "Interface\\AddOns\\AmmeNotepad\\Art\\save.png",
  target = "Interface\\AddOns\\AmmeNotepad\\Art\\target.png",
  del = "Interface\\AddOns\\AmmeNotepad\\Art\\bin.png",
  rename = "Interface\\AddOns\\AmmeNotepad\\Art\\rename.png",
  share = "Interface\\AddOns\\AmmeNotepad\\Art\\share.png",
  ok = "Interface\\AddOns\\AmmeNotepad\\Art\\checkmark.png",
  undo = "Interface\\AddOns\\AmmeNotepad\\Art\\Undo.png",
  redo = "Interface\\AddOns\\AmmeNotepad\\Art\\Redo.png",
  tabLeft = "Interface\\AddOns\\AmmeNotepad\\Art\\arrowhead_left.png",
  tabRight = "Interface\\AddOns\\AmmeNotepad\\Art\\arrowhead_right.png",
  resize = "Interface\\AddOns\\AmmeNotepad\\Art\\resize.png",
  plus = "Interface\\AddOns\\AmmeNotepad\\Art\\plus.png",
  lock = "Interface\\AddOns\\AmmeNotepad\\Art\\lock.png",
  userRaid = "Interface\\AddOns\\AmmeNotepad\\Art\\user_raid.png",
  userParty = "Interface\\AddOns\\AmmeNotepad\\Art\\user_party.png",
  folder = "Interface\\AddOns\\AmmeNotepad\\Art\\folder.png",
  pin = "Interface\\AddOns\\AmmeNotepad\\Art\\pin.png",
  unpin = "Interface\\AddOns\\AmmeNotepad\\Art\\unpin.png",
  chevLeft = "Interface\\AddOns\\AmmeNotepad\\Art\\chevron-left.png",
  chevRight = "Interface\\AddOns\\AmmeNotepad\\Art\\chevron-right.png",
  fontSize = "Interface\\AddOns\\AmmeNotepad\\Art\\font_size.png",
}

AN.UI = AN.UI or { tabButtons = {}, tabOffset = 1, history = {}, searchResults = {}, urls = {} }
local AceGUI = LibStub and LibStub("AceGUI-3.0", true)

local function dbg(fmt, ...)
  if not AN or not AN.Debug then return end
  local ok, msg = pcall(string.format, tostring(fmt or ""), ...)
  if not ok then
    msg = tostring(fmt or "")
  end
  print("|cffd9b96eAmmeNotepad dbg:|r " .. msg)
end

local function border(f)
  f.t = f:CreateTexture(nil, "BORDER"); f.t:SetPoint("TOPLEFT"); f.t:SetPoint("TOPRIGHT"); f.t:SetHeight(1); f.t:SetColorTexture(0,0,0,1)
  f.b = f:CreateTexture(nil, "BORDER"); f.b:SetPoint("BOTTOMLEFT"); f.b:SetPoint("BOTTOMRIGHT"); f.b:SetHeight(1); f.b:SetColorTexture(0,0,0,1)
  f.l = f:CreateTexture(nil, "BORDER"); f.l:SetPoint("TOPLEFT"); f.l:SetPoint("BOTTOMLEFT"); f.l:SetWidth(1); f.l:SetColorTexture(0,0,0,1)
  f.r = f:CreateTexture(nil, "BORDER"); f.r:SetPoint("TOPRIGHT"); f.r:SetPoint("BOTTOMRIGHT"); f.r:SetWidth(1); f.r:SetColorTexture(0,0,0,1)
end

local function flat(parent, alpha)
  local f = CreateFrame("Frame", nil, parent)
  f.bg = f:CreateTexture(nil, "BACKGROUND"); f.bg:SetAllPoints(); f.bg:SetColorTexture(0,0,0, alpha or 0.8)
  border(f)
  return f
end

local function mkTip()
  if AN.UI.tip then return AN.UI.tip end
  local t = CreateFrame("Frame", nil, UIParent)
  t:SetFrameStrata("TOOLTIP"); t:SetSize(140, 40); t:Hide()
  t.bg = t:CreateTexture(nil, "BACKGROUND"); t.bg:SetAllPoints(); t.bg:SetTexture("Interface\\AddOns\\AmmeNotepad\\Art\\ui_partchment_small.png")
  t.inner = CreateFrame("Frame", nil, t)
  t.inner:SetPoint("TOPLEFT", 3, -3)
  t.inner:SetPoint("BOTTOMRIGHT", -3, 3)
  border(t.inner)
  t.txt = t:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  t.txt:SetPoint("TOPLEFT", t.inner, "TOPLEFT", 4, -4)
  t.txt:SetPoint("BOTTOMRIGHT", t.inner, "BOTTOMRIGHT", -4, 4)
  t.txt:SetJustifyH("LEFT")
  t.txt:SetJustifyV("TOP")
  t.txt:SetWordWrap(true)
  t.txt:SetTextColor(1, 1, 1, 1)
  AN.UI.tip = t
  return t
end

local function ClampFrameToScreen(frame, padOverride)
  if not frame then return end
  local parent = UIParent
  local l, r, b, t = frame:GetLeft(), frame:GetRight(), frame:GetBottom(), frame:GetTop()
  local pl, pr, pb, pt = parent:GetLeft(), parent:GetRight(), parent:GetBottom(), parent:GetTop()
  if not (l and r and b and t and pl and pr and pb and pt) then return end

  local pad = tonumber(padOverride) or 4
  local dx, dy = 0, 0
  if l < pl + pad then dx = (pl + pad) - l end
  if r > pr - pad then dx = (pr - pad) - r end
  if b < pb + pad then dy = (pb + pad) - b end
  if t > pt - pad then dy = (pt - pad) - t end
  if dx == 0 and dy == 0 then return end

  local point, relTo, relPoint, x, y = frame:GetPoint(1)
  if point then
    frame:ClearAllPoints()
    frame:SetPoint(point, relTo, relPoint, (x or 0) + dx, (y or 0) + dy)
  end
end

local function ClampFrameToScreenAbsolute(frame, padOverride)
  if not frame then return end
  local parent = UIParent
  local l, r, b, t = frame:GetLeft(), frame:GetRight(), frame:GetBottom(), frame:GetTop()
  local pl, pr, pb, pt = parent:GetLeft(), parent:GetRight(), parent:GetBottom(), parent:GetTop()
  local cx, cy = frame:GetCenter()
  if not (l and r and b and t and pl and pr and pb and pt and cx and cy) then return end

  local pad = tonumber(padOverride) or 4
  local dx, dy = 0, 0
  if l < pl + pad then dx = (pl + pad) - l end
  if r > pr - pad then dx = (pr - pad) - r end
  if b < pb + pad then dy = (pb + pad) - b end
  if t > pt - pad then dy = (pt - pad) - t end
  if dx == 0 and dy == 0 then return end

  frame:ClearAllPoints()
  frame:SetPoint("CENTER", UIParent, "BOTTOMLEFT", cx + dx, cy + dy)
end

local function PositionTooltipNearOwner(tip, owner)
  if not tip or not owner then return end

  local ox, oy = owner:GetCenter()
  local ux, uy = UIParent:GetCenter()
  local putAbove = (oy and uy) and (oy <= uy) or true
  local alignRight = (ox and ux) and (ox >= ux) or false

  local selfPoint
  local ownerPoint
  local xoff, yoff = 0, 6
  if putAbove then
    selfPoint = alignRight and "BOTTOMRIGHT" or "BOTTOMLEFT"
    ownerPoint = alignRight and "TOPRIGHT" or "TOPLEFT"
    yoff = 6
  else
    selfPoint = alignRight and "TOPRIGHT" or "TOPLEFT"
    ownerPoint = alignRight and "BOTTOMRIGHT" or "BOTTOMLEFT"
    yoff = -6
  end

  tip:ClearAllPoints()
  tip:SetPoint(selfPoint, owner, ownerPoint, xoff, yoff)
  ClampFrameToScreen(tip)

  local tl, tr, tb, tt = tip:GetLeft(), tip:GetRight(), tip:GetBottom(), tip:GetTop()
  local ol, orr, ob, ot = owner:GetLeft(), owner:GetRight(), owner:GetBottom(), owner:GetTop()
  local overlaps = tl and tr and tb and tt and ol and orr and ob and ot and not (tr <= ol or tl >= orr or tt <= ob or tb >= ot)
  if overlaps then
    tip:ClearAllPoints()
    if putAbove then
      tip:SetPoint(alignRight and "TOPRIGHT" or "TOPLEFT", owner, alignRight and "BOTTOMRIGHT" or "BOTTOMLEFT", 0, -6)
    else
      tip:SetPoint(alignRight and "BOTTOMRIGHT" or "BOTTOMLEFT", owner, alignRight and "TOPRIGHT" or "TOPLEFT", 0, 6)
    end
    ClampFrameToScreen(tip)
  end
end

local function btn(parent, size, icon, tip)
  local b = CreateFrame("Button", nil, parent)
  b:SetSize(size, size)
  b.bg = b:CreateTexture(nil, "BACKGROUND"); b.bg:SetAllPoints(); b.bg:SetColorTexture(0,0,0,0.8)
  border(b)
  b.icon = b:CreateTexture(nil, "ARTWORK"); b.icon:SetPoint("TOPLEFT",2,-2); b.icon:SetPoint("BOTTOMRIGHT",-2,2); b.icon:SetTexture(icon)
  b.tip = tip
  b:SetScript("OnEnter", function(self)
    self.bg:SetColorTexture(0.12,0.12,0.12,0.9)
    local t = mkTip()
    local text = self.tip or ""
    local PAD = 14 -- 3px inset each side + 4px inner padding each side
    local MIN_W, MAX_W = 80, 320

    -- First pass: measure natural width, then clamp and re-measure wrapped height.
    t.txt:SetWidth(0.001)
    t.txt:SetText(text)
    local naturalW = math.ceil(t.txt:GetStringWidth() + 0.5)
    local contentW = math.max(MIN_W - PAD, math.min(MAX_W - PAD, naturalW))
    t.txt:SetWidth(contentW)

    local contentH = math.ceil(t.txt:GetStringHeight() + 0.5)
    local tipW = contentW + PAD
    local tipH = math.max(24, contentH + PAD)
    t:SetSize(tipW, tipH)

    PositionTooltipNearOwner(t, self)
    t:Show()
  end)
  b:SetScript("OnLeave", function(self)
    self.bg:SetColorTexture(0,0,0,0.8)
    if AN.UI.tip then AN.UI.tip:Hide() end
  end)
  return b
end

local function textBtn(parent, w, h, label)
  local b = CreateFrame("Button", nil, parent)
  b:SetSize(w, h)
  b.bg = b:CreateTexture(nil, "BACKGROUND")
  b.bg:SetAllPoints()
  b.bg:SetColorTexture(0, 0, 0, 0.8)
  border(b)
  b.txt = b:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  b.txt:SetPoint("CENTER")
  b.txt:SetText(label or "")
  b:SetScript("OnEnter", function(self) self.bg:SetColorTexture(0.12, 0.12, 0.12, 0.9) end)
  b:SetScript("OnLeave", function(self) self.bg:SetColorTexture(0, 0, 0, 0.8) end)
  return b
end

local function setTextBtnState(b, enabled, disabledTip)
  if not b then return end
  b.enabledState = enabled and true or false
  b.disabledTip = disabledTip
  b.txt:SetTextColor(enabled and 1 or 0.65, enabled and 1 or 0.65, enabled and 1 or 0.65, enabled and 1 or 0.9)
  b.bg:SetColorTexture(0, 0, 0, enabled and 0.8 or 0.6)
end

local function enableBtn(b, on)
  b:SetEnabled(on)
  b.icon:SetDesaturated(not on)
  b.icon:SetVertexColor(on and 1 or 0.5, on and 1 or 0.5, on and 1 or 0.5, 1)
  b.bg:SetColorTexture(0,0,0, on and 0.8 or 0.55)
  if b.glyph then
    b.glyph:SetAlpha(on and 1 or 0.5)
  end
end

local function setFlatControlEnabled(b, on)
  if not b then return end
  b.enabledState = on and true or false
  if b.SetEnabled then
    b:SetEnabled(on)
  end
  if b.bg then
    b.bg:SetColorTexture(0, 0, 0, on and 0.8 or 0.6)
  end
  if b.txt then
    b.txt:SetTextColor(on and 1 or 0.65, on and 1 or 0.65, on and 1 or 0.65, on and 1 or 0.9)
  end
  if b.icon then
    b.icon:SetDesaturated(not on)
    b.icon:SetVertexColor(on and 1 or 0.5, on and 1 or 0.5, on and 1 or 0.5, 1)
  end
  if b.swatch then
    b.swatch:SetAlpha(on and 1 or 0.55)
  end
end

local function setGlyph(button, glyph, fontObj, r, g, b, a)
  if not button then return end
  button.icon:SetTexture(nil)
  button.icon:SetAlpha(0)
  if not button.glyph then
    button.glyph = button:CreateFontString(nil, "OVERLAY", fontObj or "GameFontHighlight")
    button.glyph:SetPoint("CENTER", 0, 0)
  end
  button.glyph:SetFontObject(fontObj or "GameFontHighlight")
  button.glyph:SetText(glyph or "")
  button.glyph:SetTextColor(r or 1, g or 1, b or 1, a or 1)
end

local function RaisePopupAboveMain(ui, frame)
  if not frame then return end
  frame:SetFrameStrata("FULLSCREEN_DIALOG")
  if ui and ui.main then
    frame:SetFrameLevel(ui.main:GetFrameLevel() + 100)
  end
end

local function IsAnyModalShown(ui)
  if not ui then return false end
  return (ui.input and ui.input:IsShown())
    or (ui.confirm and ui.confirm:IsShown())
    or (ui.urlCopy and ui.urlCopy:IsShown())
end

function AN:GetMenuDismissLayer()
  local ui = self.UI
  if not ui then return nil end
  if ui.menuDismiss then return ui.menuDismiss end

  local d = CreateFrame("Frame", nil, UIParent)
  d:SetAllPoints(UIParent)
  d:SetFrameStrata("FULLSCREEN_DIALOG")
  d:SetFrameLevel(1)
  if d.SetPropagateMouseClicks then
    d:SetPropagateMouseClicks(true)
  end
  d:EnableMouse(false)
  d:Hide()
  d:SetScript("OnMouseDown", function() if AN and AN.TryDismissContextMenus then AN:TryDismissContextMenus() end end)
  d:SetScript("OnMouseUp", function() if AN and AN.TryDismissContextMenus then AN:TryDismissContextMenus() end end)
  d:SetScript("OnMouseWheel", function() if AN and AN.TryDismissContextMenus then AN:TryDismissContextMenus() end end)
  d:SetScript("OnHide", function(selfFrame) selfFrame:EnableMouse(false) end)
  ui.menuDismiss = d
  return d
end

local function GetContextMenus(ui)
  return {
    ui and ui.tabMenu,
    ui and ui.folderMenu,
    ui and ui.shareMenu,
    ui and ui.urlMenu,
    ui and ui.fontMenu,
    ui and ui.fontSizeMenu,
    ui and ui.pinnedMenu,
  }
end

local function IsCursorInsideFrame(frame)
  if not frame or not frame.IsShown or not frame:IsShown() then
    return false
  end
  local left, right, bottom, top = frame:GetLeft(), frame:GetRight(), frame:GetBottom(), frame:GetTop()
  if not (left and right and bottom and top) then
    return false
  end
  local scale = UIParent and UIParent.GetEffectiveScale and UIParent:GetEffectiveScale() or 1
  local cx, cy = GetCursorPosition()
  cx = cx / scale
  cy = cy / scale
  return cx >= left and cx <= right and cy >= bottom and cy <= top
end

function AN:TryDismissContextMenus()
  local ui = self.UI
  if not ui then return end
  self:HideOpenContextMenus()
end

function AN:HasOpenContextMenus()
  local ui = self.UI
  if not ui then return false end
  local menus = GetContextMenus(ui)
  for _, m in ipairs(menus) do
    if m and m:IsShown() then
      return true
    end
  end
  return false
end

function AN:SyncMenuDismissLayer()
  -- Legacy no-op: context menus now use passive click-away watcher.
end

function AN:IsMouseOverAnyOpenContextMenu()
  local ui = self.UI
  if not ui then return false end
  local menus = GetContextMenus(ui)
  for _, m in ipairs(menus) do
    if m and m:IsShown() and IsCursorInsideFrame(m) then
      return true
    end
  end
  return false
end

function AN:StartContextMenuWatcher()
  local ui = self.UI
  if not ui then return end
  if ui.contextMenuTicker then return end

  local function mouseDown(btn)
    return IsMouseButtonDown and IsMouseButtonDown(btn) or false
  end

  ui._cmPrevL = mouseDown("LeftButton")
  ui._cmPrevR = mouseDown("RightButton")
  ui._cmPrevM = mouseDown("MiddleButton")

  ui.contextMenuTicker = C_Timer.NewTicker(0.01, function(tk)
    if not AN or not AN.UI then
      tk:Cancel()
      return
    end
    local u = AN.UI
    if not AN:HasOpenContextMenus() then
      if u.contextMenuTicker then
        u.contextMenuTicker:Cancel()
        u.contextMenuTicker = nil
      end
      return
    end

    local l = mouseDown("LeftButton")
    local r = mouseDown("RightButton")
    local m = mouseDown("MiddleButton")
    local changed = (l ~= u._cmPrevL) or (r ~= u._cmPrevR) or (m ~= u._cmPrevM)
    u._cmPrevL, u._cmPrevR, u._cmPrevM = l, r, m

    if changed then
      if not AN:IsMouseOverAnyOpenContextMenu() then
        AN:HideOpenContextMenus()
      end
    end
  end)
end

function AN:StopContextMenuWatcher()
  local ui = self.UI
  if not ui then return end
  if ui.contextMenuTicker then
    ui.contextMenuTicker:Cancel()
    ui.contextMenuTicker = nil
  end
end

function AN:HideOpenContextMenus(exceptFrame)
  local ui = self.UI
  if not ui then return end
  local menus = GetContextMenus(ui)
  for _, m in ipairs(menus) do
    if m and m:IsShown() and m ~= exceptFrame then
      m:Hide()
    end
  end
  if ui.tip then ui.tip:Hide() end
  if not self:HasOpenContextMenus() then
    self:StopContextMenuWatcher()
  end
end

function AN:RegisterContextMenu(menu)
  if not menu or menu._anContextRegistered then return end
  menu._anContextRegistered = true
  menu:HookScript("OnHide", function()
    if AN and AN.HideOpenContextMenus then
      AN:HideOpenContextMenus(menu)
    end
  end)
end

function AN:ShowContextMenu(menu)
  if not menu then return end
  local ui = self.UI
  self:HideOpenContextMenus(menu)
  if ui and ui.main then
    RaisePopupAboveMain(ui, menu)
  end
  menu:Show()
  self:StartContextMenuWatcher()
end

local function sanitizeSearch(s)
  s = tostring(s or "")
  s = s:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""):gsub("|H.-|h(.-)|h", "%1"):gsub("[%z\1-\31]", "")
  if s:match("^%s*/") then return "" end
  return trim(s)
end

local function fmtNoteTimestamp(ts)
  ts = tonumber(ts)
  if not ts or ts <= 0 then
    return "Unknown"
  end
  return date("%Y-%m-%d %H:%M:%S", ts)
end

local function findUrls(text)
  local out, seen = {}, {}
  text = tostring(text or "")
  for u in text:gmatch("(https?://[%w%-%._~:/%?#%[%]@!$&'()%*+,;=]+)") do if not seen[u] then seen[u]=1; out[#out+1]=u end if #out>=20 then break end end
  if #out < 20 then
    for u in text:gmatch("(www%.[%w%-%._~:/%?#%[%]@!$&'()%*+,;=]+)") do if not seen[u] then seen[u]=1; out[#out+1]=u end if #out>=20 then break end end
  end
  return out
end

local function MoveCaretVertical(edit, ui, direction, basePos)
  local text = tostring(edit:GetText() or "")
  local len = #text
  local pos = tonumber(basePos)
  if pos == nil then
    pos = tonumber(edit:GetCursorPosition()) or 0
  end
  pos = clamp(pos, 0, len)

  -- Cursor position is 0..len (between chars). Track line bounds in that cursor-space.
  local lineStartPos = 0
  for i = pos, 1, -1 do
    if text:sub(i, i) == "\n" then
      lineStartPos = i
      break
    end
  end

  local lineEndPos = len
  for i = pos + 1, len do
    if text:sub(i, i) == "\n" then
      lineEndPos = i - 1
      break
    end
  end

  local currentCol = pos - lineStartPos
  -- Recompute horizontal intent from the current line every keypress.
  local desiredCol = currentCol

  if direction < 0 then
    if lineStartPos <= 0 then return end
    local prevEndPos = lineStartPos - 1
    if prevEndPos < 0 then prevEndPos = 0 end

    local prevStartPos = 0
    for i = prevEndPos, 1, -1 do
      if text:sub(i, i) == "\n" then
        prevStartPos = i
        break
      end
    end

    local prevLen = math.max(0, prevEndPos - prevStartPos)
    local newPos = prevStartPos + math.min(desiredCol, prevLen)
    edit:SetCursorPosition(clamp(newPos, 0, len))
    return
  end

  if lineEndPos >= len then return end

  local nextStartPos = lineEndPos + 1
  local nextEndPos = len
  for i = nextStartPos + 1, len do
    if text:sub(i, i) == "\n" then
      nextEndPos = i - 1
      break
    end
  end

  local nextLen = math.max(0, nextEndPos - nextStartPos)
  local newPos = nextStartPos + math.min(desiredCol, nextLen)
  edit:SetCursorPosition(clamp(newPos, 0, len))
end

local function hist(ui, id)
  if not ui.history[id] then ui.history[id] = { undo = {}, redo = {} } end
  return ui.history[id]
end

function AN:ShowInputDialog(title, current, accept, validate)
  local ui = self.UI
  if not ui.input then
    local f = flat(UIParent, 0.84); f:SetSize(420, 170); f:SetFrameStrata("FULLSCREEN_DIALOG"); f:SetToplevel(true); f:EnableMouse(true); f:SetMovable(true); f:RegisterForDrag("LeftButton"); f:SetScript("OnDragStart", f.StartMoving); f:SetScript("OnDragStop", f.StopMovingOrSizing); f:Hide()
    f.t = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); f.t:SetPoint("TOPLEFT", 10, -10)
    f.box = flat(f, 0.76); f.box:SetPoint("TOPLEFT", 10, -38); f.box:SetPoint("TOPRIGHT", -10, -38); f.box:SetHeight(30)
    f.e = CreateFrame("EditBox", nil, f.box); f.e:SetPoint("TOPLEFT", 6, -5); f.e:SetPoint("BOTTOMRIGHT", -6, 5); f.e:SetAutoFocus(true); f.e:SetFontObject("GameFontNormal")
    f.err = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.err:SetPoint("TOPLEFT", f.box, "BOTTOMLEFT", 2, -6)
    f.err:SetPoint("TOPRIGHT", f.box, "BOTTOMRIGHT", -2, -6)
    f.err:SetJustifyH("LEFT")
    f.err:SetTextColor(1, 0.35, 0.35, 1)
    f.ok = textBtn(f, 76, 24, "Save"); f.ok:SetPoint("BOTTOMRIGHT", -10, 10)
    f.cancel = textBtn(f, 76, 24, "Cancel"); f.cancel:SetPoint("RIGHT", f.ok, "LEFT", -8, 0); f.cancel:SetScript("OnClick", function() f:Hide() end)
    ui.input = f
  end
  local f = ui.input
  f.t:SetText(title or "Input")
  f.e:SetText(current or ""); f.e:HighlightText(0)
  f.err:SetText("")
  local function tryAccept()
    local v = trim(f.e:GetText())
    if v == "" then return end
    if validate then
      local ok, msg = validate(v)
      if not ok then
        f.err:SetText(msg or "Invalid name.")
        return
      end
    end
    f.err:SetText("")
    f:Hide()
    if accept then accept(v) end
  end
  f.ok:SetScript("OnClick", tryAccept)
  f.e:SetScript("OnEnterPressed", tryAccept)
  f.e:SetScript("OnEscapePressed", function() f:Hide() end)
  f:ClearAllPoints(); f:SetPoint("CENTER"); RaisePopupAboveMain(ui, f)
  if ui.edit and ui.edit.ClearFocus then
    ui.edit:ClearFocus()
  end
  f:Show()
  f.e:SetFocus()
  if C_Timer and C_Timer.After then
    C_Timer.After(0, function()
      if f and f:IsShown() and f.e and f.e.SetFocus then
        f.e:SetFocus()
      end
    end)
  end
end

function AN:ShowConfirmDialog(title, msg, accept, decline, acceptText, declineText, opts)
  local ui = self.UI
  if not ui.confirm then
    local f = flat(UIParent, 0.84); f:SetSize(360, 160); f:SetFrameStrata("FULLSCREEN_DIALOG"); f:SetToplevel(true); f:EnableMouse(true); f:SetMovable(true); f:RegisterForDrag("LeftButton"); f:SetScript("OnDragStart", f.StartMoving); f:SetScript("OnDragStop", f.StopMovingOrSizing); f:Hide()
    f.t = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); f.t:SetPoint("TOPLEFT", 10, -10)
    f.m = f:CreateFontString(nil, "OVERLAY", "GameFontNormal"); f.m:SetPoint("TOPLEFT", 10, -34); f.m:SetPoint("RIGHT", -10, 0); f.m:SetJustifyH("LEFT")
    f.ok = textBtn(f, 76, 24, "Yes"); f.ok:SetPoint("BOTTOMRIGHT", -10, 10)
    f.cancel = textBtn(f, 76, 24, "No"); f.cancel:SetPoint("RIGHT", f.ok, "LEFT", -8, 0); f.cancel:SetScript("OnClick", function() f:Hide() end)

    f.optWrap = CreateFrame("Button", nil, f)
    f.optWrap:SetPoint("BOTTOMLEFT", 10, 40)
    f.optWrap:SetSize(334, 18)
    f.optWrap.box = CreateFrame("Frame", nil, f.optWrap)
    f.optWrap.box:SetPoint("LEFT", 0, 0)
    f.optWrap.box:SetSize(14, 14)
    f.optWrap.box.bg = f.optWrap.box:CreateTexture(nil, "BACKGROUND")
    f.optWrap.box.bg:SetAllPoints()
    f.optWrap.box.bg:SetColorTexture(0, 0, 0, 0.82)
    border(f.optWrap.box)
    f.optWrap.dot = f.optWrap.box:CreateTexture(nil, "ARTWORK")
    f.optWrap.dot:SetPoint("TOPLEFT", 2, -2)
    f.optWrap.dot:SetPoint("BOTTOMRIGHT", -2, 2)
    f.optWrap.dot:SetTexture(ICON.ok)
    f.optWrap.dot:SetVertexColor(1, 1, 1, 1)
    f.optWrap.lbl = f.optWrap:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.optWrap.lbl:SetPoint("LEFT", f.optWrap.box, "RIGHT", 6, 0)
    f.optWrap.lbl:SetPoint("RIGHT", 0, 0)
    f.optWrap.lbl:SetJustifyH("LEFT")
    f.optWrap.state = true
    f.optWrap:SetScript("OnClick", function(selfBtn)
      selfBtn.state = not selfBtn.state
      selfBtn.dot:SetShown(selfBtn.state)
    end)
    f.optWrap:SetScript("OnEnter", function(selfBtn)
      selfBtn.box.bg:SetColorTexture(0.12, 0.12, 0.12, 0.9)
    end)
    f.optWrap:SetScript("OnLeave", function(selfBtn)
      selfBtn.box.bg:SetColorTexture(0, 0, 0, 0.82)
    end)
    f.optWrap:Hide()

    f.SetOptionChecked = function(selfFrame, checked)
      local state = checked and true or false
      selfFrame.optWrap.state = state
      selfFrame.optWrap.dot:SetShown(state)
    end
    f.GetOptionChecked = function(selfFrame)
      return selfFrame.optWrap.state and true or false
    end
    ui.confirm = f
  end
  local f = ui.confirm
  opts = opts or {}
  f.t:SetText(title or "Confirm"); f.m:SetText(msg or "")
  local t = tostring(title or ""):lower()
  if acceptText and acceptText ~= "" then
    f.ok.txt:SetText(acceptText)
  elseif t:find("delete", 1, true) then
    f.ok.txt:SetText("Delete")
  else
    f.ok.txt:SetText("Yes")
  end

  if declineText and declineText ~= "" then
    f.cancel.txt:SetText(declineText)
  elseif t:find("delete", 1, true) then
    f.cancel.txt:SetText("Cancel")
  else
    f.cancel.txt:SetText("No")
  end

  local function autoSizeDialogButton(btn, explicitWidth)
    if not btn then return end
    local minW = 76
    local maxW = 220
    local pad = 24
    local w
    if explicitWidth ~= nil then
      w = clamp(tonumber(explicitWidth) or minW, minW, maxW)
    else
      local textW = 0
      if btn.txt and btn.txt.GetStringWidth then
        textW = math.ceil((btn.txt:GetStringWidth() or 0) + 0.5)
      end
      w = clamp(textW + pad, minW, maxW)
    end
    btn:SetWidth(w)
    btn:SetHeight(24)
  end
  autoSizeDialogButton(f.ok, opts.acceptButtonWidth)
  autoSizeDialogButton(f.cancel, opts.declineButtonWidth)

  local showOption = (opts.checkboxLabel ~= nil and tostring(opts.checkboxLabel) ~= "")
  if showOption then
    f:SetHeight(186)
    f.optWrap:Show()
    f.optWrap.lbl:SetText(tostring(opts.checkboxLabel))
    f:SetOptionChecked(opts.checkboxDefault ~= false)
  else
    f:SetHeight(160)
    f.optWrap:Hide()
  end

  local function getOpt()
    if showOption then
      return f:GetOptionChecked()
    end
    return false
  end

  f.ok:SetScript("OnClick", function() f:Hide(); if accept then accept(getOpt()) end end)
  f.cancel:SetScript("OnClick", function()
    f:Hide()
    if decline then decline(getOpt()) end
  end)
  f:ClearAllPoints(); f:SetPoint("CENTER"); RaisePopupAboveMain(ui, f); f:Show()
end

function AN:ShowURLCopyDialog(url)
  local ui = self.UI
  if not ui.urlCopy then
    local f = CreateFrame("Frame", nil, UIParent); f:SetFrameStrata("FULLSCREEN_DIALOG"); f:SetToplevel(true); f:EnableMouse(true); f:SetMovable(true); f:RegisterForDrag("LeftButton"); f:SetScript("OnDragStart", f.StartMoving); f:SetScript("OnDragStop", f.StopMovingOrSizing); f:SetSize(440,130); f:Hide()
    f.bg = f:CreateTexture(nil, "BACKGROUND"); f.bg:SetAllPoints(); f.bg:SetTexture("Interface\\AddOns\\AmmeNotepad\\Art\\ui_partchment_small.png")
    f.dim = f:CreateTexture(nil, "ARTWORK"); f.dim:SetAllPoints(); f.dim:SetColorTexture(0,0,0,0.45); border(f)
    f.l = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); f.l:SetPoint("TOPLEFT", 10, -10); f.l:SetText("Copy URL")
    f.box = flat(f, 0.72); f.box:SetPoint("TOPLEFT", 10, -36); f.box:SetPoint("TOPRIGHT", -10, -36); f.box:SetHeight(32)
    f.e = CreateFrame("EditBox", nil, f.box); f.e:SetPoint("TOPLEFT",6,-5); f.e:SetPoint("BOTTOMRIGHT",-6,5); f.e:SetAutoFocus(true); f.e:SetFontObject("GameFontHighlight")
    f.c = btn(f,22,ICON.del,"Close"); f.c:SetPoint("BOTTOMRIGHT", -10, 10); f.c:SetScript("OnClick", function() f:Hide() end)
    ui.urlCopy = f
  end
  local f = ui.urlCopy; f:ClearAllPoints(); f:SetPoint("CENTER"); f.e:SetText(url or ""); f.e:HighlightText(0); RaisePopupAboveMain(ui, f); f:Show(); f.e:SetFocus()
end

function AN:ShowSharedImportPrompt(data)
  if type(data) ~= "table" then
    return
  end

  local from = tostring(data.from or "Unknown")
  local title = trim(data.title or "untitled")
  if title == "" then
    title = "untitled"
  end

  local msg = ("From: %s\nTitle: %s\n\nImport this shared note as a new read-only tab?"):format(from, title)

  local maxShared = self:GetMaxSharedNotes()
  local sharedCount = self:GetSharedNoteCount()
  if sharedCount >= maxShared then
    local _, oldest = self:GetOldestSharedNote()
    if oldest then
      msg = msg .. ("\n\nYou already have the maximum %d shared notes.\nImporting this will remove the oldest shared note:\n%s (%s)"):format(
        maxShared,
        tostring(oldest.title or "untitled"),
        fmtNoteTimestamp(oldest.createdAt)
      )
    end
  end

  self:ShowConfirmDialog(
    "Import Shared Note",
    msg,
    function()
      self:ImportSharedPayloadToUI(data)
    end,
    nil,
    "Import",
    "Cancel"
  )
end

function AN:ShowRemoteImportPrompt(from, title, shareID)
  from = tostring(from or "Unknown")
  title = trim(title or "untitled")
  shareID = tostring(shareID or "")
  if title == "" then title = "untitled" end
  if shareID == "" then return end

  local msg = ("From: %s\nTitle: %s\n\nImport this shared note as a new read-only tab?\n\nThe sender must be online with AmmeNotepad loaded for transfer to succeed."):format(from, title)

  local maxShared = self:GetMaxSharedNotes()
  local sharedCount = self:GetSharedNoteCount()
  if sharedCount >= maxShared then
    local _, oldest = self:GetOldestSharedNote()
    if oldest then
      msg = msg .. ("\n\nYou already have the maximum %d shared notes.\nImporting this will remove the oldest shared note:\n%s (%s)"):format(
        maxShared,
        tostring(oldest.title or "untitled"),
        fmtNoteTimestamp(oldest.createdAt)
      )
    end
  end

  local ui = self.UI
  if not ui.remoteImport then
    local f = flat(UIParent, 0.84)
    f:SetSize(420, 220)
    f:SetFrameStrata("FULLSCREEN_DIALOG")
    f:SetToplevel(true)
    f:EnableMouse(true)
    f:SetMovable(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)
    f:Hide()

    f.t = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    f.t:SetPoint("TOPLEFT", 10, -10)
    f.t:SetText("Import Shared Note")

    f.m = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    f.m:SetPoint("TOPLEFT", 10, -34)
    f.m:SetPoint("TOPRIGHT", -10, -34)
    f.m:SetJustifyH("LEFT")
    f.m:SetJustifyV("TOP")

    f.progressShell = flat(f, 0.72)
    f.progressShell:SetPoint("BOTTOMLEFT", 10, 46)
    f.progressShell:SetPoint("BOTTOMRIGHT", -10, 46)
    f.progressShell:SetHeight(16)
    f.progressShell:Hide()

    f.progressFill = f.progressShell:CreateTexture(nil, "ARTWORK")
    f.progressFill:SetPoint("TOPLEFT", 1, -1)
    f.progressFill:SetPoint("BOTTOMLEFT", 1, 1)
    f.progressFill:SetWidth(0)
    f.progressFill:SetColorTexture(0.95, 0.82, 0.24, 0.7)

    f.progressText = f.progressShell:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.progressText:SetPoint("CENTER")
    f.progressText:SetText("")

    f.status = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    f.status:SetPoint("TOPLEFT", f.progressShell, "BOTTOMLEFT", 2, -6)
    f.status:SetPoint("TOPRIGHT", f.progressShell, "BOTTOMRIGHT", -2, -6)
    f.status:SetJustifyH("LEFT")
    f.status:SetTextColor(1, 1, 1, 1)
    f.status:SetText("")

    f.ok = textBtn(f, 76, 24, "Import")
    f.ok:SetPoint("BOTTOMRIGHT", -10, 10)
    f.cancel = textBtn(f, 76, 24, "Cancel")
    f.cancel:SetPoint("RIGHT", f.ok, "LEFT", -8, 0)

    f.progressFrac = 0
    f.progressShell:SetScript("OnSizeChanged", function(selfShell)
      local owner = selfShell:GetParent()
      if not owner or not owner.progressFill then return end
      local width = math.max(0, (selfShell:GetWidth() - 2) * clamp(owner.progressFrac or 0, 0, 1))
      owner.progressFill:SetWidth(width)
    end)

    f:SetScript("OnHide", function(selfFrame)
      if selfFrame.reqID and AN and AN.CancelShareRequest then
        AN:CancelShareRequest(selfFrame.reqID)
      end
      selfFrame.reqID = nil
      selfFrame.importing = false
      selfFrame.requestToken = (selfFrame.requestToken or 0) + 1
    end)

    ui.remoteImport = f
  end

  local f = ui.remoteImport
  f.from = from
  f.shareID = shareID
  f.noteTitle = title
  f.m:SetText(msg)
  f.status:SetText("")
  f.progressShell:Hide()
  f.progressText:SetText("")
  f.progressFrac = 0
  f.progressFill:SetWidth(0)
  f.importing = false
  f.reqID = nil
  f.requestToken = (f.requestToken or 0) + 1
  local promptToken = f.requestToken

  local function setProgress(fraction, label)
    if not f or not f.progressShell then return end
    local frac = clamp(tonumber(fraction) or 0, 0, 1)
    f.progressFrac = frac
    local width = math.max(0, (f.progressShell:GetWidth() - 2) * frac)
    f.progressFill:SetWidth(width)
    if label then
      f.progressText:SetText(tostring(label))
    end
  end

  local function showOkOnly(statusText, isError)
    f.importing = false
    if f.reqID and AN and AN.CancelShareRequest then
      AN:CancelShareRequest(f.reqID)
    end
    f.reqID = nil
    f.progressShell:Hide()
    f.progressText:SetText("")
    f.status:SetText(tostring(statusText or ""))
    if isError then
      f.status:SetTextColor(1, 0.4, 0.4, 1)
    else
      f.status:SetTextColor(1, 1, 1, 1)
    end
    f.ok.txt:SetText("OK")
    f.ok:SetEnabled(true)
    setTextBtnState(f.ok, true)
    f.ok:SetScript("OnClick", function() f:Hide() end)
    f.cancel:Hide()
  end

  f.ok.txt:SetText("Import")
  f.ok:SetEnabled(true)
  setTextBtnState(f.ok, true)
  f.cancel:Show()
  f.cancel.txt:SetText("Cancel")
  f.cancel:SetEnabled(true)
  setTextBtnState(f.cancel, true)
  f.cancel:SetScript("OnClick", function() f:Hide() end)

  f.ok:SetScript("OnClick", function()
    if f.importing then
      return
    end
    if f.requestToken ~= promptToken then
      return
    end

    f.importing = true
    f.ok:SetEnabled(false)
    setTextBtnState(f.ok, false)
    f.cancel:SetEnabled(true)
    setTextBtnState(f.cancel, true)
    f.progressShell:Show()
    f.status:SetTextColor(1, 1, 1, 1)
    f.status:SetText("Receiving shared note...")
    setProgress(0, "0%")

    local ok, err, reqID = self:RequestSharedNote(from, shareID, title, {
      onProgress = function(cbReqID, receivedChunks, totalChunks, receivedBytes, totalBytes)
        if not f:IsShown() then return end
        if f.requestToken ~= promptToken then return end
        if tostring(cbReqID or "") ~= tostring(f.reqID or "") then return end

        local rBytes = tonumber(receivedBytes) or 0
        local tBytes = tonumber(totalBytes) or 0
        local rChunks = tonumber(receivedChunks) or 0
        local tChunks = tonumber(totalChunks) or 1

        local frac
        if tBytes > 0 then
          frac = rBytes / tBytes
        else
          frac = rChunks / math.max(1, tChunks)
        end
        frac = clamp(frac, 0, 1)
        setProgress(frac, ("%d%%"):format(math.floor(frac * 100 + 0.5)))
      end,
      onError = function(cbReqID, emsg)
        if not f:IsShown() then return end
        if f.requestToken ~= promptToken then return end
        if tostring(cbReqID or "") ~= tostring(f.reqID or "") then return end
        showOkOnly("Import failed: " .. tostring(emsg or "Unable to import shared note."), true)
      end,
      onComplete = function(cbReqID, importOK, emsg)
        if not f:IsShown() then return end
        if f.requestToken ~= promptToken then return end
        if tostring(cbReqID or "") ~= tostring(f.reqID or "") then return end

        if importOK then
          f.importing = false
          f.reqID = nil
          setProgress(1, "100%")
          f.status:SetTextColor(1, 1, 1, 1)
          f.status:SetText("Import complete.")
          f.ok:SetEnabled(false)
          setTextBtnState(f.ok, false)
          f.cancel:Hide()
          if C_Timer and C_Timer.After then
            C_Timer.After(0.15, function()
              if f and f:IsShown() and f.requestToken == promptToken then
                f:Hide()
              end
            end)
          else
            f:Hide()
          end
        else
          showOkOnly("Import failed: " .. tostring(emsg or "Unable to import shared note."), true)
        end
      end,
    })

    if not ok then
      showOkOnly("Import failed: " .. tostring(err or "Unable to request shared note."), true)
      return
    end

    f.reqID = reqID
    f.cancel:SetScript("OnClick", function()
      if f.reqID and AN and AN.CancelShareRequest then
        AN:CancelShareRequest(f.reqID)
      end
      showOkOnly("Import cancelled. No note was imported.", false)
    end)
  end)

  f:ClearAllPoints()
  f:SetPoint("CENTER")
  RaisePopupAboveMain(ui, f)
  if self.HideOpenContextMenus then
    self:HideOpenContextMenus()
  end
  f:Show()
end

function AN:ClearFeedbackMessage()
  local ui = self.UI
  if not ui then return end
  if ui.feedbackAnim and ui.feedbackAnim:IsPlaying() then
    ui.feedbackAnim:Stop()
  end
  if ui.feedback then
    ui.feedback:SetAlpha(0)
    ui.feedback:Hide()
  end
end

function AN:ShowFeedbackMessage(msg, opts)
  local ui = self.UI
  if not ui or not ui.feedback or not ui.feedbackText then return end
  msg = tostring(msg or "")
  if msg == "" then
    self:ClearFeedbackMessage()
    return
  end

  opts = opts or {}
  local fadeIn = tonumber(opts.fadeIn) or 0.5
  local hold = tonumber(opts.hold) or 3
  local fadeOut = tonumber(opts.fadeOut) or 0.5
  local r = (opts.r ~= nil) and opts.r or 1
  local g = (opts.g ~= nil) and opts.g or 1
  local b = (opts.b ~= nil) and opts.b or 1
  local a = (opts.a ~= nil) and opts.a or 1

  if not ui.feedbackAnim then
    local ag = ui.feedback:CreateAnimationGroup()
    local ain = ag:CreateAnimation("Alpha"); ain:SetOrder(1); ain:SetFromAlpha(0); ain:SetToAlpha(1)
    local ahold = ag:CreateAnimation("Alpha"); ahold:SetOrder(2); ahold:SetFromAlpha(1); ahold:SetToAlpha(1)
    local aout = ag:CreateAnimation("Alpha"); aout:SetOrder(3); aout:SetFromAlpha(1); aout:SetToAlpha(0)
    ag.ain = ain
    ag.ahold = ahold
    ag.aout = aout
    ag:SetScript("OnFinished", function()
      if ui.feedback then
        ui.feedback:SetAlpha(0)
        ui.feedback:Hide()
      end
    end)
    ui.feedbackAnim = ag
  end

  if ui.feedbackAnim:IsPlaying() then
    ui.feedbackAnim:Stop()
  end

  ui.feedbackText:SetTextColor(r, g, b, a)
  ui.feedbackText:SetText(msg)
  ui.feedback:SetAlpha(0)
  ui.feedback:Show()

  ui.feedbackAnim.ain:SetDuration(fadeIn)
  ui.feedbackAnim.ahold:SetDuration(hold)
  ui.feedbackAnim.aout:SetDuration(fadeOut)
  ui.feedbackAnim:Play()
end

function AN:UpdateSaveStatus(msg)
  self:ShowFeedbackMessage(msg, { fadeIn = 0.5, hold = 3, fadeOut = 0.5 })
end

function AN:SaveCurrentNote(auto)
  local ui = self.UI
  if not ui.main or not ui.selectedID then return false end
  local note = self:GetNoteByID(ui.selectedID); if not note then return false end
  if note.isShared then return false end
  local body = ui.edit:GetText() or ""
  local hadChange = (tostring(note.body or "") ~= body)
  self:SaveNote(note.id, body, auto)
  ui.lastText = ui.lastText or {}; ui.lastText[note.id] = body
  if not auto then
    local saveStateChanged = (note.userSaved ~= true) or (note.dirty == true)
    note.userSaved = true
    note.dirty = false
    if saveStateChanged then
      self.SessionPendingDiskSave = true
    end
  end
  if hadChange then
    self.SessionPendingDiskSave = true
  end
  self:RefreshActiveNoteMetadata()
  if not auto then
    ui.lastAutoSnapshot = ui.lastAutoSnapshot or {}
    ui.lastAutoSnapshot[note.id] = body
    self:UpdateSaveStatus("Saved at " .. date("%H:%M:%S"))
  end
  if self.RefreshPinnedNoteFrame then
    self:RefreshPinnedNoteFrame()
  end
  return true
end

function AN:ManualSaveSelected()
  local ui = self.UI
  local id = ui.selectedID; if not id then return end
  local note = self:GetNoteByID(id); if not note then return end
  if note.isShared then
    self:ShowFeedbackMessage("Shared tabs are read-only and cannot be saved.", { hold = 3.5 })
    return
  end
  local function doSave()
    self:SaveCurrentNote(false)
    self:RefreshTabs()
  end
  if note.isUntitled then
    self:ShowInputDialog("Name this note", note.title, function(name) self:RenameNote(id, name); doSave() end, function(name)
      local wanted = tostring(name):lower()
      for _, noteID in ipairs(self.DB.noteOrder or {}) do
        if noteID ~= id then
          local n = self.DB.notes and self.DB.notes[noteID]
          if n and tostring(n.title or ""):lower() == wanted then
            return false, "That name is already in use. Please choose another."
          end
        end
      end
      return true
    end)
  else
    doSave()
  end
end

function AN:RenameSelectedNote()
  local ui = self.UI
  local note = ui.selectedID and self:GetNoteByID(ui.selectedID)
  if not note then return end
  if note.isShared then
    self:ShowFeedbackMessage("Shared tabs are read-only and cannot be renamed.", { hold = 3.5 })
    return
  end
  self:ShowInputDialog("Rename note", note.title, function(name) if self:RenameNote(note.id, name) then self:RefreshTabs() end end, function(name)
    local wanted = tostring(name):lower()
    for _, noteID in ipairs(self.DB.noteOrder or {}) do
      if noteID ~= note.id then
        local n = self.DB.notes and self.DB.notes[noteID]
        if n and tostring(n.title or ""):lower() == wanted then
          return false, "That name is already in use. Please choose another."
        end
      end
    end
    return true
  end)
end

function AN:DeleteSelectedNote()
  local ui = self.UI
  local old = ui.selectedID; if not old then return end
  self:ShowConfirmDialog("Delete note", "Delete this note permanently?", function()
    local ids = self:GetVisibleNoteIDs(); local repl
    for i,id in ipairs(ids) do if id==old then repl = ids[i-1] or ids[i+1]; break end end
    self:DeleteNote(old)
    ids = self:GetVisibleNoteIDs(); repl = repl or ids[1]
    if repl then self:SelectNote(repl) else self:RefreshAllUI() end
  end)
end

function AN:CloseTabsToRight(referenceNoteID)
  if not referenceNoteID then return end
  local ui = self.UI

  -- Persist any in-progress edits before bulk-close decisions.
  self:SaveCurrentNote(true)

  local ids = self:GetVisibleNoteIDs()
  local refIndex
  for i, id in ipairs(ids) do
    if id == referenceNoteID then
      refIndex = i
      break
    end
  end
  if not refIndex then return end

  local toClose = {}
  local nonEmptyCount = 0
  local sharedCount = 0
  for i = refIndex + 1, #ids do
    local id = ids[i]
    toClose[#toClose + 1] = id
    local n = self:GetNoteByID(id)
    if n then
      if n.isShared then
        sharedCount = sharedCount + 1
      end
      if trim(n.body or "") ~= "" then
        nonEmptyCount = nonEmptyCount + 1
      end
    end
  end

  if #toClose == 0 then
    return
  end

  local function doClose()
    for _, id in ipairs(toClose) do
      self:DeleteNote(id)
    end
    if ui.selectedID and not self:GetNoteByID(ui.selectedID) then
      self:SelectNote(referenceNoteID)
    end
    self:RefreshAllUI()
  end

  local shouldConfirm = (nonEmptyCount > 3) or (sharedCount > 0)
  if shouldConfirm then
    local msg = string.format("This will close %d tab(s) to the right.", #toClose)
    if nonEmptyCount > 0 then
      msg = msg .. string.format("\n\n%d of those tab(s) contain note content.", nonEmptyCount)
    end
    if sharedCount > 0 then
      msg = msg .. string.format("\n\nWarning: %d of those tab(s) are shared read-only tabs.", sharedCount)
    end
    msg = msg .. "\n\nContinue?"
    self:ShowConfirmDialog(
      "Close Tabs to the Right",
      msg,
      doClose,
      nil,
      "Close Tabs",
      "Cancel"
    )
    return
  end

  doClose()
end

function AN:SetFolderPanelExpanded(expanded, animate)
  local ui = self.UI
  if not ui or not ui.folderPanel or not ui.main then return end
  expanded = expanded and true or false

  local panelWidth = tonumber(ui.folderPanelWidth) or 244
  local peekWidth = tonumber(ui.folderPanelPeek) or 20
  local joinOffset = tonumber(ui.folderPanelJoinOffset) or 7
  local targetOffset = expanded and (-(panelWidth - joinOffset)) or (-peekWidth)
  local panel = ui.folderPanel
  local function setOffset(x)
    panel:ClearAllPoints()
    panel:SetPoint("TOPLEFT", ui.main, "TOPLEFT", x, 0)
    panel:SetPoint("BOTTOMLEFT", ui.main, "BOTTOMLEFT", x, 0)
    ui.folderPanelOffset = x
  end

  local function finalize()
    ui.folderPanelExpanded = expanded
    if self.DB and self.DB.settings then
      self.DB.settings.folderPanelExpanded = expanded
    end
    if ui.folderPanelContent then
      if expanded then ui.folderPanelContent:Show() else ui.folderPanelContent:Hide() end
    end
    if ui.folderToggle and ui.folderToggle.icon then
      -- Collapsed: show left chevron (expands panel out to the left).
      -- Expanded: show right chevron (collapses panel back to the right).
      ui.folderToggle.icon:SetTexture(expanded and ICON.chevRight or ICON.chevLeft)
      ui.folderToggle.tip = expanded and "Collapse folders panel" or "Expand folders panel"
      ui.folderToggle:SetSize(14, 30)
      ui.folderToggle:ClearAllPoints()
      if expanded then
        ui.folderToggle:SetPoint("RIGHT", panel, "RIGHT", -5, 0)
      else
        ui.folderToggle:SetPoint("LEFT", panel, "LEFT", 10, 0)
      end
    end
    if self.RefreshFolderPanel then
      self:RefreshFolderPanel()
    end
  end

  if ui.folderSlideTicker then
    ui.folderSlideTicker:Cancel()
    ui.folderSlideTicker = nil
  end

  if not animate then
    setOffset(targetOffset)
    finalize()
    return
  end

  local start = ui.folderPanelOffset or (ui.folderPanelExpanded and (-(panelWidth - joinOffset)) or (-peekWidth))
  local steps = 8
  local idx = 0
  ui.folderSlideTicker = C_Timer.NewTicker(0.02, function(tk)
    idx = idx + 1
    local p = idx / steps
    local x = start + ((targetOffset - start) * p)
    setOffset(x)
    if idx >= steps then
      tk:Cancel()
      ui.folderSlideTicker = nil
      setOffset(targetOffset)
      finalize()
    end
  end)
end

function AN:SelectFolder(folderName)
  if not self:SetActiveFolder(folderName) then return false end
  local ui = self.UI
  if not ui then return false end

  ui.selectedFolder = self:GetActiveFolder()
  self:SaveCurrentNote(true)
  if self.RefreshFolderPanel then
    self:RefreshFolderPanel()
  end

  local ids = self:GetVisibleNoteIDs()
  if #ids > 0 then
    self:SelectNote(ids[1])
  else
    ui.selectedID = nil
    if ui.edit then
      ui.block = true
      ui.edit:SetText("")
      ui.block = false
    end
    self:RefreshAllUI()
  end
  return true
end

local function validateFolderNameInput(self, inputName, excludeName)
  local raw = tostring(inputName or "")
  local maxLen = (self.GetMaxFolderNameLength and self:GetMaxFolderNameLength()) or 36
  local trimmed = trim(raw)
  local clean = (self.SanitizeFolderNameInput and self:SanitizeFolderNameInput(raw)) or trimmed

  if trimmed == "" then
    return false, "Folder name cannot be empty."
  end
  if #trimmed > maxLen then
    return false, string.format("Folder name must be %d characters or fewer.", maxLen)
  end
  if raw:find("[%c]") or raw:find("|", 1, true) then
    return false, "Folder name contains invalid characters."
  end
  if clean == "" then
    return false, "Folder name cannot be empty."
  end
  if self:IsFolderNameInUse(clean, excludeName) then
    return false, "That folder name is already in use."
  end
  return true
end

function AN:CreateFolderPrompt()
  if self:IsAtMaxNotes() then
    self:ShowFeedbackMessage("Cannot create folder. Maximum notes reached.", { r = 1, g = 0.4, b = 0.4, hold = 4 })
    return
  end
  self:ShowInputDialog("Create folder", "", function(name)
    local folderName, err = self:CreateFolder(name)
    if not folderName then
      self:ShowFeedbackMessage("Create folder failed. See chat.", { r = 1, g = 0.4, b = 0.4, hold = 4 })
      if self.PrintMessage then self:PrintMessage("Create folder failed: " .. tostring(err or "Unknown error"), true) end
      return
    end
    local id, createErr = self:CreateNote(nil, folderName)
    if not id then
      self:ShowFeedbackMessage("Folder created but starter note failed. See chat.", { r = 1, g = 0.4, b = 0.4, hold = 4 })
      if self.PrintMessage then self:PrintMessage("Create starter note failed: " .. tostring(createErr or "Unknown error"), true) end
    end
    self:SetFolderPanelExpanded(true, true)
    self:SelectFolder(folderName)
    if id then self:SelectNote(id) end
    self:RefreshAllUI()
  end, function(name)
    return validateFolderNameInput(self, name, nil)
  end)
end

function AN:RenameSelectedFolderPrompt()
  local ui = self.UI
  local folder = (ui and ui.selectedFolder) or self:GetActiveFolder()
  if not folder then return end
  self:ShowInputDialog("Rename folder", folder, function(name)
    local ok, err = self:RenameFolder(folder, name)
    if not ok then
      self:ShowFeedbackMessage("Rename folder failed. See chat.", { r = 1, g = 0.4, b = 0.4, hold = 4 })
      if self.PrintMessage then self:PrintMessage("Rename folder failed: " .. tostring(err or "Unknown error"), true) end
      return
    end
    local cleanName = (self.SanitizeFolderNameInput and self:SanitizeFolderNameInput(name)) or trim(name)
    self:SelectFolder(cleanName)
    self:RefreshAllUI()
  end, function(name)
    return validateFolderNameInput(self, name, folder)
  end)
end

function AN:DeleteSelectedFolderPrompt()
  local ui = self.UI
  local folder = (ui and ui.selectedFolder) or self:GetActiveFolder()
  if not folder then return end

  local info, err = self:GetFolderDeleteInfo(folder)
  if not info then
    self:ShowFeedbackMessage("Cannot delete folder.", { r = 1, g = 0.4, b = 0.4, hold = 3 })
    if self.PrintMessage then self:PrintMessage(tostring(err or "Folder delete is unavailable."), true) end
    return
  end

  local msg
  local checkboxLabel = nil
  local checkboxDefault = true
  if info.nonEmptyCount > 0 then
    msg = string.format(
      "Delete folder '%s'?\n\nThis folder has %d non-empty note(s) and %d empty note(s).\nEmpty notes will be removed.",
      info.folder,
      info.nonEmptyCount,
      tonumber(info.emptyCount or 0)
    )
    checkboxLabel = string.format("Move all non-empty notes into '%s'", tostring(info.destination or "default"))
  else
    msg = string.format(
      "Delete folder '%s'?\n\nAll notes in this folder are empty and will be removed.",
      info.folder
    )
  end

  self:ShowConfirmDialog("Delete Folder", msg, function(moveChecked)
    local moveNonEmpty = (info.nonEmptyCount > 0) and (moveChecked ~= false)
    local ok, destination, _ = self:DeleteFolder(folder, { moveNonEmpty = moveNonEmpty })
    if not ok then
      self:ShowFeedbackMessage("Delete folder failed. See chat.", { r = 1, g = 0.4, b = 0.4, hold = 4 })
      return
    end
    self:SelectFolder(destination or self:GetActiveFolder())
    self:RefreshAllUI()
  end, nil, "Delete", "Cancel", {
    checkboxLabel = checkboxLabel,
    checkboxDefault = checkboxDefault,
  })
end

function AN:ShowFolderContextMenu(folderName)
  local ui = self.UI
  if not ui then return end
  self:CloseShareMenu()
  self:CloseAppearanceMenus()
  if not ui.folderMenu then
    local m = flat(UIParent, 0.9); m:SetFrameStrata("FULLSCREEN_DIALOG"); m:SetSize(168, 94); m:SetToplevel(true); m:SetClampedToScreen(true); m:EnableMouse(true); m:Hide(); m.btn = {}
    local labels = {"Create", "Rename", "Delete"}
    for i, l in ipairs(labels) do
      local b = CreateFrame("Button", nil, m); b:SetPoint("TOPLEFT", 6, -6-((i-1)*28)); b:SetPoint("TOPRIGHT", -6, -6-((i-1)*28)); b:SetHeight(24)
      b.bg = b:CreateTexture(nil, "BACKGROUND"); b.bg:SetAllPoints(); b.bg:SetColorTexture(0, 0, 0, 0.75); border(b)
      b.txt = b:CreateFontString(nil, "OVERLAY", "GameFontNormal")
      b.txt:SetPoint("LEFT", 8, 0)
      b.txt:SetPoint("RIGHT", -6, 0)
      b.txt:SetJustifyH("LEFT")
      b.txt:SetText(l)
      b.enabledState = true
      b.disabledTip = nil
      b:SetScript("OnEnter", function(selfBtn)
        if selfBtn.enabledState then
          selfBtn.bg:SetColorTexture(0.12, 0.12, 0.12, 0.9)
          return
        end
        selfBtn.bg:SetColorTexture(0, 0, 0, 0.6)
        if selfBtn.disabledTip and selfBtn.disabledTip ~= "" then
          local t = mkTip()
          local PAD = 14
          local MIN_W, MAX_W = 80, 320
          t.txt:SetWidth(0.001); t.txt:SetText(selfBtn.disabledTip)
          local naturalW = math.ceil(t.txt:GetStringWidth() + 0.5)
          local contentW = math.max(MIN_W - PAD, math.min(MAX_W - PAD, naturalW))
          t.txt:SetWidth(contentW)
          local contentH = math.ceil(t.txt:GetStringHeight() + 0.5)
          t:SetSize(contentW + PAD, math.max(24, contentH + PAD))
          PositionTooltipNearOwner(t, selfBtn)
          t:Show()
        end
      end)
      b:SetScript("OnLeave", function(selfBtn)
        selfBtn.bg:SetColorTexture(0, 0, 0, selfBtn.enabledState and 0.75 or 0.6)
        if AN.UI.tip then AN.UI.tip:Hide() end
      end)
      m.btn[l] = b
    end
    m.SetButtonState = function(_, button, enabled, disabledTip)
      if not button then return end
      button.enabledState = enabled and true or false
      button.disabledTip = disabledTip
      button.txt:SetTextColor(enabled and 1 or 0.65, enabled and 1 or 0.65, enabled and 1 or 0.65, enabled and 1 or 0.9)
      button.bg:SetColorTexture(0, 0, 0, enabled and 0.75 or 0.6)
    end
    ui.folderMenu = m
    self:RegisterContextMenu(m)
  end

  local m = ui.folderMenu
  local folderCount = #(self:GetFolders())
  m:SetButtonState(m.btn.Create, true, nil)
  m:SetButtonState(m.btn.Rename, true, nil)
  m:SetButtonState(m.btn.Delete, folderCount > 1, folderCount <= 1 and "Disabled: you cannot delete the last folder.")
  m.btn.Create:SetScript("OnClick", function() m:Hide(); self:CreateFolderPrompt() end)
  m.btn.Rename:SetScript("OnClick", function() if not m.btn.Rename.enabledState then return end; m:Hide(); self:SelectFolder(folderName); self:RenameSelectedFolderPrompt() end)
  m.btn.Delete:SetScript("OnClick", function() if not m.btn.Delete.enabledState then return end; m:Hide(); self:SelectFolder(folderName); self:DeleteSelectedFolderPrompt() end)

  local s = UIParent:GetEffectiveScale(); local x,y = GetCursorPosition()
  m:ClearAllPoints(); m:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x/s, y/s); self:ShowContextMenu(m)
end

function AN:ClosePanelMenusForSideActions()
  if self.HideOpenContextMenus then
    self:HideOpenContextMenus()
  end
  if self.CloseAppearanceMenus then
    self:CloseAppearanceMenus()
  end
  if self.CloseShareMenu then
    self:CloseShareMenu()
  end
  if self.CloseFolderMenu then
    self:CloseFolderMenu()
  end
end

function AN:GetCurrentGroupMembers()
  local members = {}
  local seen = {}
  local kind = "party"

  local function getUnitClassColor(unit)
    local classTag = select(2, UnitClass(unit))
    if not classTag or classTag == "" then
      return 0.20, 0.20, 0.20
    end
    if C_ClassColor and C_ClassColor.GetClassColor then
      local c = C_ClassColor.GetClassColor(classTag)
      if c then
        return c.r or 0.20, c.g or 0.20, c.b or 0.20
      end
    end
    local c = RAID_CLASS_COLORS and RAID_CLASS_COLORS[classTag]
    if c then
      return c.r or 0.20, c.g or 0.20, c.b or 0.20
    end
    return 0.20, 0.20, 0.20
  end

  local function addUnit(unit)
    if not unit or not UnitExists(unit) then return end
    local isPlayer = UnitIsPlayer(unit) and true or false
    local name, realm
    if UnitFullName then
      name, realm = UnitFullName(unit)
    else
      name = UnitName(unit)
      realm = nil
    end
    name = tostring(name or "")
    if name == "" then return end
    realm = tostring(realm or "")
    if realm == "" and isPlayer then
      realm = tostring(GetNormalizedRealmName() or GetRealmName() or "")
    end
    local full = (realm ~= "") and (name .. "-" .. realm) or name
    local key = full:lower()
    if seen[key] then return end
    seen[key] = true
    local cr, cg, cb = getUnitClassColor(unit)
    members[#members + 1] = {
      name = name,
      realm = realm,
      fullName = full,
      unit = unit,
      isPlayer = isPlayer,
      classR = cr,
      classG = cg,
      classB = cb,
    }
  end

  if IsInRaid() then
    kind = "raid"
    local count = GetNumGroupMembers() or 0
    for i = 1, count do
      addUnit("raid" .. i)
    end
    addUnit("player")
  elseif IsInGroup() then
    kind = "party"
    local count = GetNumSubgroupMembers() or 0
    for i = 1, count do
      addUnit("party" .. i)
    end
    addUnit("player")
  end

  return members, kind
end

function AN:InsertGroupMemberNameAtCursor(fullName, includeRealm)
  local ui = self.UI
  if not ui or not ui.edit then return end
  local note = ui.selectedID and self:GetNoteByID(ui.selectedID)
  if not note or note.isShared then return end

  local text = tostring(ui.edit:GetText() or "")
  local pos = clamp(tonumber(ui.edit:GetCursorPosition()) or 0, 0, #text)
  local full = tostring(fullName or "")
  if full == "" then return end
  local short = full:match("^([^%-]+)") or full
  local insertName = includeRealm and full or short

  local prefix = ""
  if pos > 0 then
    local prev = text:sub(pos, pos)
    if prev ~= "" and not prev:match("%s") then
      prefix = " "
    end
  end

  if ui.edit.SetFocus then
    ui.edit:SetFocus()
  end
  ui.edit:Insert(prefix .. insertName)
end

function AN:SetFolderPanelMode(mode)
  local ui = self.UI
  if not ui then return end
  local normalized = (tostring(mode or ""):lower() == "group") and "group" or "folders"
  ui.folderPanelMode = normalized
  if ui.UpdateFolderModeTabs then
    ui:UpdateFolderModeTabs()
  end
  if self.RefreshFolderPanel then
    self:RefreshFolderPanel()
  end
end

function AN:RefreshFolderPanel()
  local ui = self.UI
  if not ui or not ui.folderPanel or not ui.folderListShell or not ui.folderListContent then return end

  local ROW_H = 28
  local ROW_STEP = 29
  local mode = (ui.folderPanelMode == "group") and "group" or "folders"
  ui.folderPanelMode = mode
  if ui.UpdateFolderModeTabs then
    ui:UpdateFolderModeTabs()
  end

  local showingFolders = (mode == "folders")
  if ui.folderBtnRow then
    if showingFolders then ui.folderBtnRow:Show() else ui.folderBtnRow:Hide() end
  end
  if ui.folderStatsLine1 then
    if showingFolders then ui.folderStatsLine1:Show() else ui.folderStatsLine1:Hide() end
  end
  if ui.folderStatsLine2 then
    if showingFolders then ui.folderStatsLine2:Show() else ui.folderStatsLine2:Hide() end
  end
  if ui.groupHelpText then
    if showingFolders then ui.groupHelpText:Hide() else ui.groupHelpText:Show() end
  end
  if ui.groupEmptyText then
    ui.groupEmptyText:Hide()
  end

  ui.folderListShell:ClearAllPoints()
  if showingFolders and ui.folderBtnRow then
    ui.folderListShell:SetPoint("TOPLEFT", ui.folderBtnRow, "BOTTOMLEFT", 0, -6)
    ui.folderListShell:SetPoint("TOPRIGHT", ui.folderBtnRow, "BOTTOMRIGHT", 0, -6)
  elseif ui.folderTabsRow then
    ui.folderListShell:SetPoint("TOPLEFT", ui.folderTabsRow, "BOTTOMLEFT", 0, -6)
    ui.folderListShell:SetPoint("TOPRIGHT", ui.folderTabsRow, "BOTTOMRIGHT", 0, -6)
  end
  ui.folderListShell:SetPoint("BOTTOMLEFT", ui.folderPanelContent, "BOTTOMLEFT", 0, 44)
  ui.folderListShell:SetPoint("BOTTOMRIGHT", ui.folderPanelContent, "BOTTOMRIGHT", -30, 44)

  ui.folderRows = ui.folderRows or {}
  ui.groupRows = ui.groupRows or {}
  if ui.folderScroll then
    ui.folderListContent:SetWidth(math.max(100, (ui.folderScroll:GetWidth() or 100) - 2))
  end

  for i = 1, #ui.folderRows do
    ui.folderRows[i]:Hide()
  end
  for i = 1, #ui.groupRows do
    ui.groupRows[i]:Hide()
  end

  local visibleCount = 0
  if showingFolders then
    local folders = self:GetFolders()
    local active = self:GetActiveFolder()
    ui.selectedFolder = active

    for i, folderName in ipairs(folders) do
      local row = ui.folderRows[i]
      if not row then
        row = CreateFrame("Button", nil, ui.folderListContent)
        row:SetHeight(ROW_H)
        row.bg = row:CreateTexture(nil, "BACKGROUND"); row.bg:SetAllPoints(); row.bg:SetColorTexture(0, 0, 0, 0.78); border(row)
        row.icon = row:CreateTexture(nil, "ARTWORK")
        row.icon:SetSize(25, 25)
        row.icon:SetPoint("LEFT", 3, 0)
        row.icon:SetTexture(ICON.folder)
        row.icon:SetVertexColor(0.96, 0.92, 0.72, 0.95)
        row.txt = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        row.txt:SetPoint("LEFT", row.icon, "RIGHT", 4, 0)
        row.txt:SetPoint("RIGHT", -6, 0)
        row.txt:SetJustifyH("LEFT")
        row.txt:SetWordWrap(false)
        if row.txt.SetMaxLines then row.txt:SetMaxLines(1) end
        row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
        row:SetScript("OnClick", function(selfRow, btnName)
          if btnName == "RightButton" then
            AN:ShowFolderContextMenu(selfRow.folderName)
          else
            local activeFolder = AN:GetActiveFolder()
            if tostring(selfRow.folderName or ""):lower() ~= tostring(activeFolder or ""):lower() and AN.CloseFolderMenu then
              AN:CloseFolderMenu()
            end
            AN:SelectFolder(selfRow.folderName)
          end
        end)
        row:SetScript("OnEnter", function(selfRow)
          local alpha = tonumber(selfRow.baseAlpha) or 0.65
          selfRow.bg:SetColorTexture(0, 0, 0, math.min(0.95, alpha + 0.12))
          if selfRow.isTruncated then
            local t = mkTip()
            local text = tostring(selfRow.fullName or "")
            local PAD = 14
            local MIN_W, MAX_W = 80, 320
            t.txt:SetWidth(0.001)
            t.txt:SetText(text)
            local naturalW = math.ceil(t.txt:GetStringWidth() + 0.5)
            local contentW = math.max(MIN_W - PAD, math.min(MAX_W - PAD, naturalW))
            t.txt:SetWidth(contentW)
            local contentH = math.ceil(t.txt:GetStringHeight() + 0.5)
            t:SetSize(contentW + PAD, math.max(24, contentH + PAD))
            PositionTooltipNearOwner(t, selfRow)
            t:Show()
          end
        end)
        row:SetScript("OnLeave", function(selfRow)
          selfRow.bg:SetColorTexture(0, 0, 0, tonumber(selfRow.baseAlpha) or 0.65)
          if AN.UI.tip then AN.UI.tip:Hide() end
        end)
        ui.folderRows[i] = row
      end
      row.folderName = folderName
      row.fullName = folderName
      row:SetPoint("TOPLEFT", 3, -3 - ((i-1) * ROW_STEP))
      row:SetPoint("TOPRIGHT", -3, -3 - ((i-1) * ROW_STEP))
      local usable = ((ui.folderListContent and ui.folderListContent:GetWidth()) or 120) - 25 - 3 - 4 - 6
      local shown = truncToWidth(row.txt, folderName, math.max(20, usable))
      row.txt:SetText(shown)
      row.isTruncated = (shown ~= folderName)
      if tostring(folderName):lower() == tostring(active):lower() then
        row.baseAlpha = 0.88
        row.bg:SetColorTexture(0, 0, 0, row.baseAlpha)
        row.txt:SetTextColor(1.0, 0.86, 0.20, 1)
        row.icon:SetVertexColor(1.0, 0.90, 0.40, 1)
      else
        row.baseAlpha = 0.65
        row.bg:SetColorTexture(0, 0, 0, row.baseAlpha)
        row.txt:SetTextColor(1, 1, 1, 0.9)
        row.icon:SetVertexColor(0.96, 0.92, 0.72, 0.85)
      end
      row:Show()
      visibleCount = visibleCount + 1
    end

    local canDelete = (#folders > 1)
    if ui.folderDeleteBtn then
      ui.folderDeleteBtn.enabledState = canDelete
      ui.folderDeleteBtn:SetEnabled(canDelete)
      if ui.folderDeleteBtn.icon then
        ui.folderDeleteBtn.icon:SetDesaturated(not canDelete)
        ui.folderDeleteBtn.icon:SetVertexColor(canDelete and 1 or 0.55, canDelete and 1 or 0.55, canDelete and 1 or 0.55, 1)
      end
      if ui.folderDeleteBtn.bg then
        ui.folderDeleteBtn.bg:SetColorTexture(0, 0, 0, canDelete and 0.8 or 0.6)
      end
      if ui.folderDeleteBtn.actionIcon then
        ui.folderDeleteBtn.actionIcon:SetDesaturated(not canDelete)
        ui.folderDeleteBtn.actionIcon:SetVertexColor(canDelete and 1 or 0.55, canDelete and 1 or 0.55, canDelete and 1 or 0.55, 1)
      end
      if canDelete then
        ui.folderDeleteBtn.tip = ui.folderDeleteBtn.baseTip or "Delete selected folder"
      else
        ui.folderDeleteBtn.tip = (ui.folderDeleteBtn.baseTip or "Delete selected folder") .. "\n\nDisabled: you cannot delete the last folder."
      end
    end

    if ui.folderStatsLine1 or ui.folderStatsLine2 then
      local totalNotes = 0
      local unsavedNotes = 0
      if self.DB and type(self.DB.noteOrder) == "table" then
        for _, noteID in ipairs(self.DB.noteOrder) do
          local note = self.DB.notes and self.DB.notes[noteID]
          if note then
            totalNotes = totalNotes + 1
            if (not note.isShared) and (note.userSaved ~= true or note.dirty == true) then
              unsavedNotes = unsavedNotes + 1
            end
          end
        end
      end
      local folderCount = #(self:GetFolders() or {})
      if ui.folderStatsLine1 then
        ui.folderStatsLine1:SetText(string.format("%d notes across %d folders.", totalNotes, folderCount))
      end
      if ui.folderStatsLine2 then
        ui.folderStatsLine2:SetText(string.format("%d unsaved notes", unsavedNotes))
      end
      if self.UpdateFolderUnsavedPulse then
        self:UpdateFolderUnsavedPulse(unsavedNotes)
      end
    end
  else
    local members, groupKind = self:GetCurrentGroupMembers()
    local groupIcon = (groupKind == "raid") and ICON.userRaid or ICON.userParty
    local displayCount = #members
    if displayCount == 0 then
      if ui.groupEmptyText then
        ui.groupEmptyText:SetText("You are not currenty in a group")
        ui.groupEmptyText:Show()
      end
    else
      if ui.groupEmptyText then
        ui.groupEmptyText:Hide()
      end
    end

    for i, entry in ipairs(members) do
      local row = ui.groupRows[i]
      if not row then
        row = CreateFrame("Button", nil, ui.folderListContent)
        row:SetHeight(ROW_H)
        row.bg = row:CreateTexture(nil, "BACKGROUND"); row.bg:SetAllPoints(); row.bg:SetColorTexture(0, 0, 0, 0.78); border(row)
        row.iconShadow = row:CreateTexture(nil, "BORDER")
        row.iconShadow:SetSize(26, 26)
        row.iconShadow:SetPoint("LEFT", 4, -1)
        row.iconShadow:SetVertexColor(0, 0, 0, 0.5)
        row.icon = row:CreateTexture(nil, "ARTWORK")
        row.icon:SetSize(25, 25)
        row.icon:SetPoint("LEFT", 3, 0)
        row.txt = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        row.txt:SetPoint("LEFT", row.icon, "RIGHT", 4, 0)
        row.txt:SetPoint("RIGHT", -6, 0)
        row.txt:SetJustifyH("LEFT")
        row.txt:SetWordWrap(false)
        if row.txt.SetMaxLines then row.txt:SetMaxLines(1) end
        row:RegisterForClicks("LeftButtonUp")
        row:SetScript("OnClick", function(selfRow)
          if selfRow.isPlaceholder then return end
          if AN.ClosePanelMenusForSideActions then
            AN:ClosePanelMenusForSideActions()
          end
          local includeRealm = IsShiftKeyDown and IsShiftKeyDown() or false
          AN:InsertGroupMemberNameAtCursor(selfRow.fullName, includeRealm)
        end)
        row:SetScript("OnEnter", function(selfRow)
          local alpha = tonumber(selfRow.baseAlpha) or 0.65
          selfRow.bg:SetColorTexture(selfRow.bgR or 0.20, selfRow.bgG or 0.20, selfRow.bgB or 0.20, math.min(0.95, alpha + 0.12))
          if selfRow.isTruncated then
            local t = mkTip()
            local text = tostring(selfRow.fullName or "")
            local PAD = 14
            local MIN_W, MAX_W = 80, 320
            t.txt:SetWidth(0.001)
            t.txt:SetText(text)
            local naturalW = math.ceil(t.txt:GetStringWidth() + 0.5)
            local contentW = math.max(MIN_W - PAD, math.min(MAX_W - PAD, naturalW))
            t.txt:SetWidth(contentW)
            local contentH = math.ceil(t.txt:GetStringHeight() + 0.5)
            t:SetSize(contentW + PAD, math.max(24, contentH + PAD))
            PositionTooltipNearOwner(t, selfRow)
            t:Show()
          end
        end)
        row:SetScript("OnLeave", function(selfRow)
          selfRow.bg:SetColorTexture(selfRow.bgR or 0, selfRow.bgG or 0, selfRow.bgB or 0, tonumber(selfRow.baseAlpha) or 0.65)
          if AN.UI.tip then AN.UI.tip:Hide() end
        end)
        ui.groupRows[i] = row
      end
      row.fullName = entry.fullName
      row:SetPoint("TOPLEFT", 3, -3 - ((i-1) * ROW_STEP))
      row:SetPoint("TOPRIGHT", -3, -3 - ((i-1) * ROW_STEP))
      if row.iconShadow then
        row.iconShadow:SetTexture(groupIcon)
        row.iconShadow:SetVertexColor(0, 0, 0, 0.5)
      end
      row.icon:SetTexture(groupIcon)
      row.icon:SetVertexColor(0.96, 0.92, 0.72, 0.95)
      row.baseAlpha = 0.65
      row.bgR = tonumber(entry.classR) or 0.20
      row.bgG = tonumber(entry.classG) or 0.20
      row.bgB = tonumber(entry.classB) or 0.20
      row.bg:SetColorTexture(row.bgR, row.bgG, row.bgB, row.baseAlpha)
      local plainFull = tostring(entry.fullName or "")
      local usable = math.max(20, ((ui.folderListContent and ui.folderListContent:GetWidth()) or 120) - 25 - 3 - 4 - 6)
      local shownPlain = truncToWidth(row.txt, plainFull, usable)
      local displayText = shownPlain
      local namePart = tostring(entry.name or "")
      local realmPart = tostring(entry.realm or "")
      if namePart ~= "" and realmPart ~= "" then
        local prefix = namePart .. "-"
        if shownPlain:sub(1, #prefix) == prefix then
          local realmShown = shownPlain:sub(#prefix + 1)
          if realmShown ~= "" then
            displayText = namePart .. "|cff9a9a9a-" .. realmShown .. "|r"
          end
        end
      end
      row.txt:SetText(displayText)
      row.txt:SetTextColor(1, 1, 1, 0.92)
      row.isTruncated = (shownPlain ~= plainFull)
      row:Show()
      visibleCount = visibleCount + 1
    end

    if ui.folderStatsPulseTicker then
      ui.folderStatsPulseTicker:Cancel()
      ui.folderStatsPulseTicker = nil
    end
    if ui.folderStatsLine2 then
      ui.folderStatsLine2:SetTextColor(0.90, 0.90, 0.90, 0.9)
    end
  end

  local viewH = ui.folderScroll and (ui.folderScroll:GetHeight() or 0) or 0
  local contentH = math.max(viewH, (visibleCount * ROW_STEP) + 6)
  ui.folderListContent:SetHeight(contentH)
  if ui.folderScroll then
    if ui.folderScroll.UpdateScrollChildRect then
      ui.folderScroll:UpdateScrollChildRect()
    end
    local sb = ui.folderScroll.ScrollBar or (ui.folderScroll.GetName and _G[ui.folderScroll:GetName() .. "ScrollBar"])
    if sb then
      local range = ui.folderScroll:GetVerticalScrollRange() or 0
      sb:SetMinMaxValues(0, range)
      sb:SetValue(math.min(range, ui.folderScroll:GetVerticalScroll() or 0))
      if sb.SetEnabled then sb:SetEnabled(range > 1) end
      sb:SetAlpha((range > 1) and 1 or 0.45)
      if not sb:IsShown() then sb:Show() end
    end
  end
end

function AN:UpdateFolderUnsavedPulse(unsavedCount)
  local ui = self.UI
  if not ui then return end
  local line = ui.folderStatsLine2
  if not line then return end

  local hasUnsaved = (tonumber(unsavedCount) or 0) > 0
  local baseR, baseG, baseB, baseA = 0.90, 0.90, 0.90, 0.9
  local glowR, glowG, glowB, glowA = 1.00, 0.84, 0.20, 1.0

  if not hasUnsaved then
    if ui.folderStatsPulseTicker then
      ui.folderStatsPulseTicker:Cancel()
      ui.folderStatsPulseTicker = nil
    end
    line:SetTextColor(baseR, baseG, baseB, baseA)
    return
  end

  if ui.folderStatsPulseTicker then
    return
  end

  local period = 2.0
  local t0 = GetTime and GetTime() or 0
  ui.folderStatsPulseTicker = C_Timer.NewTicker(0.05, function(tk)
    if not AN or not AN.UI then
      tk:Cancel()
      return
    end
    local u = AN.UI
    if not u.folderStatsLine2 then
      tk:Cancel()
      u.folderStatsPulseTicker = nil
      return
    end
    local now = GetTime and GetTime() or 0
    local phase = ((now - t0) / period) * (2 * math.pi)
    local mix = (math.sin(phase) + 1) * 0.5
    local r = baseR + ((glowR - baseR) * mix)
    local g = baseG + ((glowG - baseG) * mix)
    local b = baseB + ((glowB - baseB) * mix)
    local a = baseA + ((glowA - baseA) * mix)
    u.folderStatsLine2:SetTextColor(r, g, b, a)
  end)
end

function AN:InsertTargetName()
  local note = self.UI.selectedID and self:GetNoteByID(self.UI.selectedID)
  if note and note.isShared then return end
  if not UnitExists("target") then return end
  local n = UnitName("target"); if not n or n=="" then return end
  self.UI.edit:Insert(n)
end

function AN:UpdateTargetButtonState()
  local b = self.UI.targetBtn
  if not b then return end

  local selected = self.UI.selectedID and self:GetNoteByID(self.UI.selectedID)
  local readOnly = selected and selected.isShared
  local hasTarget = UnitExists("target") and not readOnly
  b:SetEnabled(hasTarget)
  if readOnly then
    b.tip = "Insert current target name\n\nDisabled: this shared tab is read-only."
  else
    b.tip = hasTarget and "Insert current target name" or "Insert current target name\n\nDisabled: you do not currently have a target selected."
  end
  -- Keep the same panel/border look; only fade the icon when disabled.
  b.bg:SetColorTexture(0, 0, 0, 0.8)
  if hasTarget then
    b.icon:SetDesaturated(false)
    b.icon:SetVertexColor(1, 1, 1, 1)
  else
    b.icon:SetDesaturated(true)
    b.icon:SetVertexColor(0.5, 0.5, 0.5, 1)
  end
end

function AN:RefreshNoteActionStates()
  local ui = self.UI
  if not ui then return end

  local note = ui.selectedID and self:GetNoteByID(ui.selectedID)
  local readOnly = note and note.isShared

  if ui.saveBtn then
    enableBtn(ui.saveBtn, not readOnly)
    ui.saveBtn.tip = readOnly and "Save current note\n\nDisabled: shared tabs are read-only." or "Save current note"
  end
  if ui.renameBtn then
    enableBtn(ui.renameBtn, not readOnly)
    ui.renameBtn.tip = readOnly and "Rename current note\n\nDisabled: shared tabs are read-only." or "Rename current note"
  end
  if ui.undoBtn then
    enableBtn(ui.undoBtn, not readOnly)
  end
  if ui.redoBtn then
    enableBtn(ui.redoBtn, not readOnly)
  end
  if ui.shareBtn then
    enableBtn(ui.shareBtn, note ~= nil)
    ui.shareBtn.tip = "Share note"
  end
  if ui.pinBtn then
    enableBtn(ui.pinBtn, note ~= nil)
    if note then
      if self:IsNotePinned(note.id) then
        if ui.pinBtn.icon then
          ui.pinBtn.icon:SetTexture(ICON.unpin or ICON.pin or ICON.lock)
        end
        ui.pinBtn.tip = "Unpin this note from screen"
      else
        if ui.pinBtn.icon then
          ui.pinBtn.icon:SetTexture(ICON.pin or ICON.lock)
        end
        ui.pinBtn.tip = "Pin this note to screen"
      end
    else
      if ui.pinBtn.icon then
        ui.pinBtn.icon:SetTexture(ICON.pin or ICON.lock)
      end
      ui.pinBtn.tip = "Pin this note to screen"
    end
  end

  self:UpdateTargetButtonState()
  self:RefreshAppearanceControls()
  self:SyncEditorReadOnlyInputState(note)
end

function AN:SyncEditorReadOnlyInputState(note)
  local ui = self.UI
  if not ui or not ui.edit then return end

  if type(note) ~= "table" then
    note = ui.selectedID and self:GetNoteByID(ui.selectedID)
  end
  local readOnly = (type(note) == "table" and note.isShared == true) or false
  if ui.readOnlyInputLocked == readOnly then
    return
  end
  ui.readOnlyInputLocked = readOnly

  -- Hard input lock for shared notes: keep mouse selection, block keyboard edits.
  if ui.edit.EnableKeyboard then
    ui.edit:EnableKeyboard(not readOnly)
  end

  if readOnly then
    if ui.caretTicker then
      ui.caretTicker:Cancel()
      ui.caretTicker = nil
    end
    if ui.caret and ui.caret.Hide then
      ui.caret:Hide()
    end
  end
end

local function OpenNativeColorPicker(initial, onChanged, onCancelled)
  if not ColorPickerFrame then return end
  local base = {
    r = clamp(tonumber(initial and initial.r) or 1, 0, 1),
    g = clamp(tonumber(initial and initial.g) or 1, 0, 1),
    b = clamp(tonumber(initial and initial.b) or 1, 0, 1),
  }

  if ColorPickerFrame.SetupColorPickerAndShow then
    local info = {
      r = base.r,
      g = base.g,
      b = base.b,
      hasOpacity = false,
      opacity = 0,
    }
    info.swatchFunc = function()
      local r, g, b = ColorPickerFrame:GetColorRGB()
      if onChanged then onChanged(r, g, b) end
    end
    info.cancelFunc = function(previousValues)
      local prev = previousValues or base
      if onCancelled then
        onCancelled(prev.r or base.r, prev.g or base.g, prev.b or base.b)
      end
    end
    ColorPickerFrame:SetupColorPickerAndShow(info)
    return
  end

  ColorPickerFrame.func = function()
    local r, g, b = ColorPickerFrame:GetColorRGB()
    if onChanged then onChanged(r, g, b) end
  end
  ColorPickerFrame.cancelFunc = function(previousValues)
    local prev = previousValues or base
    if onCancelled then
      onCancelled(prev.r or base.r, prev.g or base.g, prev.b or base.b)
    end
  end
  ColorPickerFrame.hasOpacity = false
  ColorPickerFrame.opacity = 0
  ColorPickerFrame:SetColorRGB(base.r, base.g, base.b)
  ColorPickerFrame:Hide()
  ColorPickerFrame:Show()
end

function AN:ApplyNoteAppearanceToEditor(note)
  local ui = self.UI
  if not ui or not ui.edit then return end
  if type(note) ~= "table" then
    note = ui.selectedID and self:GetNoteByID(ui.selectedID)
  end

  local appearance = self:GetNoteAppearance(note)
  local fontPath = self:ResolveFontPath(appearance.fontName)
  local fontSize = tonumber(appearance.fontSize) or 12
  local color = appearance.fontColor or { r = 0.95, g = 0.95, b = 0.95 }

  local applied = false
  if ui.edit.SetFont then
    applied = pcall(ui.edit.SetFont, ui.edit, fontPath, fontSize, "")
  end
  if (not applied) and ui.edit.SetFont then
    pcall(ui.edit.SetFont, ui.edit, "Fonts\\FRIZQT__.TTF", fontSize, "")
  end
  if ui.edit.SetTextColor then
    ui.edit:SetTextColor(color.r, color.g, color.b, 1)
  end
  if ui.caret and ui.caret.SetColorTexture then
    ui.caret:SetColorTexture(color.r, color.g, color.b, 0.95)
  end
  if ui.updateEditMetrics then
    ui.updateEditMetrics()
  end
end

function AN:RefreshAppearanceControls()
  local ui = self.UI
  if not ui then return end

  local note = ui.selectedID and self:GetNoteByID(ui.selectedID)
  local appearance = self:GetNoteAppearance(note)
  local readOnly = note and note.isShared
  local hasNote = note ~= nil

  if ui.fontBtn and ui.fontBtn.txt then
    local fullName = tostring(appearance.fontName or "Friz Quadrata TT")
    local shown = fullName
    if ui.fontBtn.GetWidth then
      local maxW = math.max(30, (ui.fontBtn:GetWidth() or 180) - 12)
      shown = truncToWidth(ui.fontBtn.txt, fullName, maxW)
    end
    ui.fontBtn.txt:SetText(shown)
    ui.fontBtn.tip = "Font: " .. fullName
    setFlatControlEnabled(ui.fontBtn, hasNote and (not readOnly))
    if readOnly then
      ui.fontBtn.tip = ui.fontBtn.tip .. "\n\nDisabled: shared tabs are read-only."
    end
  end

  if ui.fontSizeBtn then
    local sizeText = tostring(appearance.fontSize or 12) .. " pt"
    ui.fontSizeBtn.tip = "Text size (" .. sizeText .. ")"
    setFlatControlEnabled(ui.fontSizeBtn, hasNote and (not readOnly))
    if readOnly then
      ui.fontSizeBtn.tip = ui.fontSizeBtn.tip .. "\n\nDisabled: shared tabs are read-only."
    end
  end

  if ui.fontColorBtn then
    local c = appearance.fontColor or { r = 0.95, g = 0.95, b = 0.95 }
    if ui.fontColorBtn.swatch then
      ui.fontColorBtn.swatch:SetColorTexture(c.r, c.g, c.b, 1)
    end
    ui.fontColorBtn.tip = "Text color"
    setFlatControlEnabled(ui.fontColorBtn, hasNote and (not readOnly))
    if readOnly then
      ui.fontColorBtn.tip = ui.fontColorBtn.tip .. "\n\nDisabled: shared tabs are read-only."
    end
  end

  self:ApplyNoteAppearanceToEditor(note)
end

function AN:SetSelectedNoteFontName(fontName)
  local ui = self.UI
  local id = ui and ui.selectedID
  if not id then return end
  local ok = self:SetNoteAppearance(id, { fontName = tostring(fontName or "") })
  if ok then
    self:RefreshTabs()
    self:RefreshAppearanceControls()
    self:RefreshActiveNoteMetadata()
    self:RefreshPinnedNoteFrame()
  end
end

function AN:SetSelectedNoteFontSize(fontSize)
  local ui = self.UI
  local id = ui and ui.selectedID
  if not id then return end
  local ok = self:SetNoteAppearance(id, { fontSize = tonumber(fontSize) or 12 })
  if ok then
    self:RefreshTabs()
    self:RefreshAppearanceControls()
    self:RefreshActiveNoteMetadata()
    self:RefreshPinnedNoteFrame()
  end
end

function AN:SetSelectedNoteFontColor(r, g, b)
  local ui = self.UI
  local id = ui and ui.selectedID
  if not id then return end
  local ok = self:SetNoteAppearance(id, {
    fontColor = { r = r, g = g, b = b },
  })
  if ok then
    self:RefreshTabs()
    self:RefreshAppearanceControls()
    self:RefreshActiveNoteMetadata()
    self:RefreshPinnedNoteFrame()
  end
end

function AN:CloseAppearanceMenus(exceptMenu)
  local ui = self.UI
  if not ui then return end
  local menus = {
    ui.fontMenu,
    ui.fontSizeMenu,
  }
  for _, menu in ipairs(menus) do
    if menu and menu:IsShown() and menu ~= exceptMenu then
      menu:Hide()
    end
  end
end

function AN:CloseShareMenu()
  local ui = self.UI
  if not ui then return end
  if ui.shareMenu and ui.shareMenu:IsShown() then
    ui.shareMenu:Hide()
  end
end

function AN:CloseFolderMenu()
  local ui = self.UI
  if not ui then return end
  if ui.folderMenu and ui.folderMenu:IsShown() then
    ui.folderMenu:Hide()
  end
end

function AN:ShowFontSizeMenu(owner)
  local ui = self.UI
  if not ui then return end
  local note = ui.selectedID and self:GetNoteByID(ui.selectedID)
  if not note or note.isShared then return end
  self:CloseFolderMenu()

  if not ui.fontSizeMenu then
    local m = flat(UIParent, 0.9)
    m:SetFrameStrata("FULLSCREEN_DIALOG")
    m:SetToplevel(true)
    m:SetClampedToScreen(true)
    m:EnableMouse(true)
    if m.SetClipsChildren then
      m:SetClipsChildren(true)
    end
    m:Hide()
    m.rows = {}
    ui.fontSizeMenu = m
    self:RegisterContextMenu(m)
  end

  local m = ui.fontSizeMenu
  local anchor = owner or ui.fontSizeBtn
  if m:IsShown() and m._owner == anchor then
    m:Hide()
    return
  end
  self:CloseAppearanceMenus(m)
  local sizes = self:GetNoteFontSizeChoices() or { 8, 10, 11, 12, 13, 14 }
  local rowH = 22
  m:SetSize(92, (#sizes * rowH) + 8)

  for i = 1, #m.rows do
    m.rows[i]:Hide()
  end

  local current = tonumber((self:GetNoteAppearance(note) or {}).fontSize) or 12
  for i, size in ipairs(sizes) do
    local row = m.rows[i]
    if not row then
      row = CreateFrame("Button", nil, m)
      row:SetHeight(rowH)
      row.bg = row:CreateTexture(nil, "BACKGROUND")
      row.bg:SetAllPoints()
      row.bg:SetColorTexture(0, 0, 0, 0.75)
      border(row)
      row.txt = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
      row.txt:SetPoint("LEFT", 6, 0)
      row.txt:SetPoint("RIGHT", -6, 0)
      row.txt:SetJustifyH("LEFT")
      row:SetScript("OnEnter", function(selfBtn)
        selfBtn.bg:SetColorTexture(0.12, 0.12, 0.12, 0.9)
      end)
      row:SetScript("OnLeave", function(selfBtn)
        selfBtn.bg:SetColorTexture(0, 0, 0, 0.75)
      end)
      m.rows[i] = row
    end
    row.sizeValue = tonumber(size) or 12
    row:ClearAllPoints()
    row:SetPoint("TOPLEFT", 4, -4 - ((i - 1) * rowH))
    row:SetPoint("TOPRIGHT", -4, -4 - ((i - 1) * rowH))
    row.txt:SetText(string.format("%d pt", row.sizeValue))
    if row.sizeValue == current then
      row.txt:SetTextColor(1.0, 0.86, 0.20, 1)
    else
      row.txt:SetTextColor(1, 1, 1, 1)
    end
    row:SetScript("OnClick", function(selfBtn)
      m:Hide()
      AN:SetSelectedNoteFontSize(selfBtn.sizeValue)
    end)
    row:Show()
  end

  m._owner = anchor
  m:ClearAllPoints()
  m:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -2)
  self:ShowContextMenu(m)
end

function AN:ShowFontMenu(owner)
  local ui = self.UI
  if not ui then return end
  local note = ui.selectedID and self:GetNoteByID(ui.selectedID)
  if not note or note.isShared then return end
  self:CloseFolderMenu()

  if not ui.fontMenu then
    local m = flat(UIParent, 0.9)
    m:SetFrameStrata("FULLSCREEN_DIALOG")
    m:SetToplevel(true)
    m:SetClampedToScreen(true)
    m:EnableMouse(true)
    if m.SetClipsChildren then
      m:SetClipsChildren(true)
    end
    m:Hide()
    m:SetSize(240, 220)

    m.scroll = CreateFrame("ScrollFrame", nil, m, "UIPanelScrollFrameTemplate")
    m.scroll:SetPoint("TOPLEFT", 6, -6)
    m.scroll:SetPoint("BOTTOMRIGHT", -24, 6)
    if m.scroll.SetClipsChildren then
      m.scroll:SetClipsChildren(true)
    end
    m.scroll:EnableMouseWheel(true)
    m.scroll:SetScript("OnMouseWheel", function(selfScroll, delta)
      local step = 18
      local range = selfScroll:GetVerticalScrollRange() or 0
      local target = (selfScroll:GetVerticalScroll() or 0) - (delta * step)
      if target < 0 then target = 0 end
      if target > range then target = range end
      selfScroll:SetVerticalScroll(target)
    end)

    m.child = CreateFrame("Frame", nil, m.scroll)
    m.child:SetSize(200, 10)
    if m.child.SetClipsChildren then
      m.child:SetClipsChildren(true)
    end
    m.scroll:SetScrollChild(m.child)
    m.rows = {}
    ui.fontMenu = m
    self:RegisterContextMenu(m)
  end

  local m = ui.fontMenu
  local anchor = owner or ui.fontBtn
  if m:IsShown() and m._owner == anchor then
    m:Hide()
    return
  end
  self:CloseAppearanceMenus(m)
  local fonts = self:GetAvailableFonts()
  local rowH = 22
  local maxVisible = 10
  local visible = math.max(1, math.min(maxVisible, #fonts))
  local menuH = (visible * rowH) + 12
  m:SetHeight(menuH)

  for i = 1, #m.rows do
    m.rows[i]:Hide()
  end

  local childH = math.max(visible * rowH, (#fonts * rowH) + 2)
  m.child:SetSize(210, childH)

  local currentName = tostring((self:GetNoteAppearance(note) or {}).fontName or "")
  for i, name in ipairs(fonts) do
    local row = m.rows[i]
    if not row then
      row = CreateFrame("Button", nil, m.child)
      row:SetHeight(rowH)
      row.bg = row:CreateTexture(nil, "BACKGROUND")
      row.bg:SetAllPoints()
      row.bg:SetColorTexture(0, 0, 0, 0.75)
      border(row)
      row.txt = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
      row.txt:SetPoint("LEFT", 6, 0)
      row.txt:SetPoint("RIGHT", -6, 0)
      row.txt:SetJustifyH("LEFT")
      row:SetScript("OnEnter", function(selfBtn)
        selfBtn.bg:SetColorTexture(0.12, 0.12, 0.12, 0.9)
      end)
      row:SetScript("OnLeave", function(selfBtn)
        selfBtn.bg:SetColorTexture(0, 0, 0, 0.75)
      end)
      m.rows[i] = row
    end
    row.fontName = tostring(name)
    row:ClearAllPoints()
    row:SetPoint("TOPLEFT", 0, -((i - 1) * rowH))
    row:SetPoint("TOPRIGHT", 0, -((i - 1) * rowH))
    row.txt:SetFontObject("GameFontNormalSmall")
    local fontPath = self:ResolveFontPath(row.fontName)
    pcall(row.txt.SetFont, row.txt, fontPath, 12, "")
    row.txt:SetText(row.fontName)
    if row.fontName:lower() == currentName:lower() then
      row.txt:SetTextColor(1.0, 0.86, 0.20, 1)
    else
      row.txt:SetTextColor(1, 1, 1, 1)
    end
    row:SetScript("OnClick", function(selfBtn)
      m:Hide()
      AN:SetSelectedNoteFontName(selfBtn.fontName)
    end)
    row:Show()
  end

  if m.scroll and m.scroll.SetVerticalScroll then
    m.scroll:SetVerticalScroll(0)
  end
  m._owner = anchor
  m:ClearAllPoints()
  m:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -2)
  self:ShowContextMenu(m)
end

function AN:Undo()
  local ui = self.UI; local id = ui.selectedID; if not id then return end
  local note = self:GetNoteByID(id)
  if note and note.isShared then return end
  local h = hist(ui,id); if #h.undo==0 then return end
  local cur = ui.edit:GetText() or ""; h.redo[#h.redo+1] = cur
  local prev = table.remove(h.undo)
  ui.block = true; ui.edit:SetText(prev); ui.block = false
  ui.lastText[id] = prev; self:SaveCurrentNote(true)
end

function AN:Redo()
  local ui = self.UI; local id = ui.selectedID; if not id then return end
  local note = self:GetNoteByID(id)
  if note and note.isShared then return end
  local h = hist(ui,id); if #h.redo==0 then return end
  local cur = ui.edit:GetText() or ""; h.undo[#h.undo+1] = cur
  local nxt = table.remove(h.redo)
  ui.block = true; ui.edit:SetText(nxt); ui.block = false
  ui.lastText[id] = nxt; self:SaveCurrentNote(true)
end

function AN:SelectNote(id)
  local ui = self.UI
  local note = self:GetNoteByID(id); if not note then return end
  if not ui then return end
  if note.folder then
    self:SetActiveFolder(note.folder)
    ui.selectedFolder = self:GetActiveFolder()
  end
  if not ui.main or not ui.edit then
    ui.selectedID = id
    ui.lastText = ui.lastText or {}
    ui.lastText[id] = note.body or ""
    ui.lastAutoSnapshot = ui.lastAutoSnapshot or {}
    ui.lastAutoSnapshot[id] = note.body or ""
    return
  end
  if ui.selectedID and ui.selectedID ~= id then self:SaveCurrentNote(true) end
  ui.selectedID = id
  ui.block = true; ui.edit:SetText(note.body or ""); ui.block = false
  local function resetEditorViewport()
    if ui.edit and ui.edit.SetCursorPosition then
      ui.edit:SetCursorPosition(0)
    end
    if ui.scroll and ui.scroll.SetVerticalScroll then
      ui.scroll:SetVerticalScroll(0)
      local sb = ui.scroll.ScrollBar
      if sb and sb.SetValue then
        sb:SetValue(0)
      end
    end
  end
  resetEditorViewport()
  if C_Timer and C_Timer.After then
    C_Timer.After(0, function()
      if not AN or not AN.UI or AN.UI.selectedID ~= id then return end
      resetEditorViewport()
      if (not IsAnyModalShown(AN.UI)) and AN.UI.main and AN.UI.main:IsShown() and AN.UI.edit and AN.UI.edit.SetFocus then
        AN.UI.edit:SetFocus()
      end
    end)
  end
  ui.lastText = ui.lastText or {}; ui.lastText[id] = note.body or ""
  ui.lastAutoSnapshot = ui.lastAutoSnapshot or {}
  ui.lastAutoSnapshot[id] = note.body or ""
  hist(ui,id)
  self:RefreshTabs(); self:ApplySearchHighlightToCurrentNote(); self:RefreshURLState(); self:RefreshActiveNoteMetadata(); self:RefreshNoteActionStates()
end

function AN:ApplySearchHighlightToCurrentNote()
  local ui = self.UI
  local note = ui.selectedID and self:GetNoteByID(ui.selectedID); if not note then return end
  local q = sanitizeSearch(ui.search:GetText())
  if #q < 3 or self.DB.settings.searchMode ~= "content" then return end
  local s,e = (note.body or ""):lower():find(q:lower(), 1, true)
  if s and e then ui.edit:SetCursorPosition(e); ui.edit:HighlightText(s-1,e) end
end

function AN:BuildSearchResults(q)
  local ui = self.UI
  local out = {}
  if #q >= 3 then
    for _,id in ipairs(self:GetVisibleNoteIDs()) do
      local n = self:GetNoteByID(id)
      if n then
        if self.DB.settings.searchMode == "name" then
          if tostring(n.title or ""):lower():find(q:lower(),1,true) then out[#out+1] = {id=id, t=n.title or "untitled", s="Name match"} end
        else
          local body = tostring(n.body or "")
          local a,b = body:lower():find(q:lower(),1,true)
          if a then out[#out+1] = {id=id, t=n.title or "untitled", s=(body:sub(math.max(1,a-16), math.min(#body,b+24)):gsub("\n"," "))} end
        end
      end
      if #out >= 15 then break end
    end
  end
  ui.searchResults = out
end

function AN:RefreshSearchDropdown()
  local ui = self.UI
  local rows = ui.searchResults or {}
  ui.searchDropBtns = ui.searchDropBtns or {}
  for i=1,#ui.searchDropBtns do ui.searchDropBtns[i]:Hide() end
  if #rows == 0 then ui.searchDrop:Hide(); return end
  ui.searchDrop:SetHeight(math.min(200, #rows * 22 + 6)); ui.searchDrop:Show()
  for i,row in ipairs(rows) do
    local b = ui.searchDropBtns[i]
    if not b then
      b = CreateFrame("Button", nil, ui.searchDrop); b:SetHeight(20)
      b.bg = b:CreateTexture(nil, "BACKGROUND"); b.bg:SetAllPoints(); b.bg:SetColorTexture(0,0,0,0.78); border(b)
      b.txt = b:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); b.txt:SetPoint("LEFT",6,0); b.txt:SetPoint("RIGHT",-6,0); b.txt:SetJustifyH("LEFT")
      ui.searchDropBtns[i] = b
    end
    b:SetPoint("TOPLEFT",3,-3-((i-1)*22)); b:SetPoint("TOPRIGHT",-3,-3-((i-1)*22))
    b.row = row; b.txt:SetText(trunc(row.t,22) .. ": " .. trunc(row.s,38))
    b:SetScript("OnClick", function(selfBtn)
      ui.searchDrop:Hide(); AN:SelectNote(selfBtn.row.id); if AN.DB.settings.searchMode == "content" then AN:ApplySearchHighlightToCurrentNote() end
    end)
    b:Show()
  end
end

function AN:RefreshURLState()
  local ui = self.UI
  if not ui.main or not ui.main:IsShown() then return end
  local note = ui.selectedID and self:GetNoteByID(ui.selectedID)
  ui.urls = findUrls((note and note.body) or ui.edit:GetText() or "")
  if #ui.urls > 0 then ui.urlBtn:Show(); ui.urlBtn.tip = "Detected URLs ("..#ui.urls..")" else ui.urlBtn:Hide() end
end

function AN:RefreshActiveNoteMetadata()
  local ui = self.UI
  if not ui or not ui.metaCreated or not ui.metaUpdated then return end

  local note = ui.selectedID and self:GetNoteByID(ui.selectedID)
  if not note then
    ui.metaCreated:SetText("Created by: -  |  Created: -")
    ui.metaUpdated:SetText("Last updated by: -  |  Updated: -")
    return
  end

  local createdBy = tostring(note.createdBy or "Unknown")
  local updatedBy = tostring(note.updatedBy or createdBy)
  local createdAt = fmtNoteTimestamp(note.createdAt)
  local updatedAt = fmtNoteTimestamp(note.updatedAt)

  ui.metaCreated:SetText(("Created by: %s  |  Created: %s"):format(createdBy, createdAt))
  ui.metaUpdated:SetText(("Last updated by: %s  |  Updated: %s"):format(updatedBy, updatedAt))
end

function AN:ShowURLListMenu()
  local ui = self.UI
  if #ui.urls == 0 then return end
  if not ui.urlMenu then
    local m = flat(UIParent, 0.9); m:SetFrameStrata("DIALOG"); m:SetSize(360,220); m:Hide(); m.rows = {}; ui.urlMenu = m
    self:RegisterContextMenu(m)
  end
  local m = ui.urlMenu
  for i=1,#m.rows do m.rows[i]:Hide() end
  local c = math.min(#ui.urls, 8); m:SetHeight(c*24+10)
  for i=1,c do
    local b = m.rows[i]
    if not b then
      b = CreateFrame("Button", nil, m); b:SetHeight(22)
      b.bg = b:CreateTexture(nil, "BACKGROUND"); b.bg:SetAllPoints(); b.bg:SetColorTexture(0,0,0,0.76); border(b)
      b.txt = b:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); b.txt:SetPoint("LEFT",6,0); b.txt:SetPoint("RIGHT",-6,0); b.txt:SetJustifyH("LEFT")
      m.rows[i] = b
    end
    b:SetPoint("TOPLEFT",5,-5-((i-1)*24)); b:SetPoint("TOPRIGHT",-5,-5-((i-1)*24))
    b.url = ui.urls[i]; b.txt:SetText(trunc(ui.urls[i], 60))
    b:SetScript("OnClick", function(selfBtn) m:Hide(); AN:ShowURLCopyDialog(selfBtn.url) end)
    b:Show()
  end
  m:ClearAllPoints(); m:SetPoint("TOPRIGHT", ui.searchShell, "BOTTOMRIGHT", 0, -2); self:ShowContextMenu(m)
end

function AN:ShowShareMenu(owner)
  local ui = self.UI
  if not ui then return end
  self:CloseFolderMenu()
  self:CloseAppearanceMenus()
  local note = ui.selectedID and self:GetNoteByID(ui.selectedID)
  if not note then return end

  if not ui.shareMenu then
    local m = flat(UIParent, 0.9)
    m:SetFrameStrata("FULLSCREEN_DIALOG")
    m:SetSize(190, 64)
    m:SetToplevel(true)
    m:SetClampedToScreen(true)
    m:EnableMouse(true)
    m:Hide()

    local function makeRow(text, y)
      local b = CreateFrame("Button", nil, m)
      b:SetPoint("TOPLEFT", 6, y)
      b:SetPoint("TOPRIGHT", -6, y)
      b:SetHeight(24)
      b.bg = b:CreateTexture(nil, "BACKGROUND")
      b.bg:SetAllPoints()
      b.bg:SetColorTexture(0, 0, 0, 0.75)
      border(b)
      b.txt = b:CreateFontString(nil, "OVERLAY", "GameFontNormal")
      b.txt:SetPoint("LEFT", 8, 0)
      b.txt:SetPoint("RIGHT", -6, 0)
      b.txt:SetJustifyH("LEFT")
      b.txt:SetText(text)
      b:SetScript("OnEnter", function(selfBtn)
        selfBtn.bg:SetColorTexture(0.12, 0.12, 0.12, 0.9)
        if selfBtn.tip and selfBtn.tip ~= "" then
          local t = mkTip()
          local PAD = 14
          local MIN_W, MAX_W = 80, 320
          t.txt:SetWidth(0.001)
          t.txt:SetText(selfBtn.tip)
          local naturalW = math.ceil(t.txt:GetStringWidth() + 0.5)
          local contentW = math.max(MIN_W - PAD, math.min(MAX_W - PAD, naturalW))
          t.txt:SetWidth(contentW)
          local contentH = math.ceil(t.txt:GetStringHeight() + 0.5)
          t:SetSize(contentW + PAD, math.max(24, contentH + PAD))
          PositionTooltipNearOwner(t, selfBtn)
          t:Show()
        end
      end)
      b:SetScript("OnLeave", function(selfBtn)
        selfBtn.bg:SetColorTexture(0, 0, 0, 0.75)
        if AN.UI.tip then AN.UI.tip:Hide() end
      end)
      return b
    end

    m.textRow = makeRow("As text in chat", -6)
    m.linkRow = makeRow("As a Import Link", -34)
    ui.shareMenu = m
    self:RegisterContextMenu(m)
  end

  local m = ui.shareMenu
  if ui.main then
    m:SetFrameLevel(ui.main:GetFrameLevel() + 100)
  end

  local canText, lineCount = self:CanShareNoteAsText(note.id)
  local canLink = true

  local function setRowState(row, enabled, tip)
    row.enabled = enabled and true or false
    row.tip = tip or ""
    row:SetEnabled(true)
    row.txt:SetTextColor(enabled and 1 or 0.65, enabled and 1 or 0.65, enabled and 1 or 0.65, enabled and 1 or 0.9)
    row.bg:SetColorTexture(0, 0, 0, enabled and 0.75 or 0.6)
  end

  setRowState(
    m.textRow,
    canText,
    canText and "Shares each note line to chat (max 10 lines)." or ("Disabled: this note has " .. tostring(lineCount) .. " lines. Maximum is " .. tostring(self:GetMaxTextShareLines()) .. ".")
  )
  setRowState(
    m.linkRow,
    canLink,
    "Generates a clickable import link.\nOnly works for users with AmmeNotepad.\nSender must be online when recipient imports."
  )

  m.textRow:SetScript("OnClick", function()
    if not m.textRow.enabled then return end
    m:Hide()
    local ok, err = AN:ShareNoteAsText(note.id)
    if ok then
      AN:ShowFeedbackMessage("Shared as text in chat.", { hold = 3 })
    else
      local emsg = tostring(err or "Failed to share as text.")
      AN:ShowFeedbackMessage("Share failed. See chat.", { r = 1, g = 0.4, b = 0.4, hold = 4 })
      if AN.PrintMessage then
        AN:PrintMessage("Share as text failed: " .. emsg, true)
      else
        print("|cffd9b96eAmmeNotepad:|r |cffff6666Share as text failed: " .. emsg .. "|r")
      end
    end
  end)
  m.linkRow:SetScript("OnClick", function()
    if not m.linkRow.enabled then return end
    m:Hide()
    local ok, err = AN:ShareNoteAsImportLink(note.id)
    if ok then
      AN:ShowFeedbackMessage("Shared as import link.", { hold = 3 })
    else
      local emsg = tostring(err or "Failed to share as link.")
      AN:ShowFeedbackMessage("Share link failed. See chat.", { r = 1, g = 0.4, b = 0.4, hold = 4 })
      if AN.PrintMessage then
        AN:PrintMessage("Share as import link failed: " .. emsg, true)
      else
        print("|cffd9b96eAmmeNotepad:|r |cffff6666Share as import link failed: " .. emsg .. "|r")
      end
    end
  end)

  m:ClearAllPoints()
  m:SetPoint("TOPLEFT", owner or ui.shareBtn, "BOTTOMLEFT", 0, -2)
  self:ShowContextMenu(m)
end

function AN:ShowTabContextMenu(id)
  local ui = self.UI
  if not ui then return end
  self:CloseShareMenu()
  self:CloseAppearanceMenus()
  if not ui.tabMenu then
    local m = flat(UIParent, 0.9); m:SetFrameStrata("FULLSCREEN_DIALOG"); m:SetSize(180, 150); m:SetToplevel(true); m:SetClampedToScreen(true); m:EnableMouse(true); m:Hide(); m.btn = {}
    local labels = {"Save","Rename","Delete","Pin to screen","Discard all notes to the right"}
    for i,l in ipairs(labels) do
      local b = CreateFrame("Button", nil, m); b:SetPoint("TOPLEFT",6,-6-((i-1)*28)); b:SetPoint("TOPRIGHT",-6,-6-((i-1)*28)); b:SetHeight(24)
      b.bg = b:CreateTexture(nil, "BACKGROUND"); b.bg:SetAllPoints(); b.bg:SetColorTexture(0,0,0,0.75); border(b)
      b.txt = b:CreateFontString(nil, "OVERLAY", "GameFontNormal")
      b.txt:SetPoint("LEFT", 8, 0)
      b.txt:SetPoint("RIGHT", -6, 0)
      b.txt:SetJustifyH("LEFT")
      b.txt:SetText(l)
      b.enabledState = true
      b.disabledTip = nil
      b:SetScript("OnEnter", function(selfBtn)
        if selfBtn.enabledState then
          selfBtn.bg:SetColorTexture(0.12, 0.12, 0.12, 0.9)
          return
        end
        selfBtn.bg:SetColorTexture(0, 0, 0, 0.6)
        if selfBtn.disabledTip and selfBtn.disabledTip ~= "" then
          local t = mkTip()
          local PAD = 14
          local MIN_W, MAX_W = 80, 320
          t.txt:SetWidth(0.001)
          t.txt:SetText(selfBtn.disabledTip)
          local naturalW = math.ceil(t.txt:GetStringWidth() + 0.5)
          local contentW = math.max(MIN_W - PAD, math.min(MAX_W - PAD, naturalW))
          t.txt:SetWidth(contentW)
          local contentH = math.ceil(t.txt:GetStringHeight() + 0.5)
          t:SetSize(contentW + PAD, math.max(24, contentH + PAD))
          PositionTooltipNearOwner(t, selfBtn)
          t:Show()
        end
      end)
      b:SetScript("OnLeave", function(selfBtn)
        if selfBtn.enabledState then
          selfBtn.bg:SetColorTexture(0, 0, 0, 0.75)
        else
          selfBtn.bg:SetColorTexture(0, 0, 0, 0.6)
        end
        if AN.UI.tip then AN.UI.tip:Hide() end
      end)
      m.btn[l] = b
    end
    m.SetButtonState = function(_, button, enabled, disabledTip)
      if not button then return end
      button.enabledState = enabled and true or false
      button.disabledTip = disabledTip
      button.txt:SetTextColor(enabled and 1 or 0.65, enabled and 1 or 0.65, enabled and 1 or 0.65, enabled and 1 or 0.9)
      button.bg:SetColorTexture(0, 0, 0, enabled and 0.75 or 0.6)
    end
    ui.tabMenu = m
    self:RegisterContextMenu(m)
  end
  local m = ui.tabMenu
  if self.UI.main then
    m:SetFrameLevel(self.UI.main:GetFrameLevel() + 100)
  end
  local note = self:GetNoteByID(id)
  local isShared = note and note.isShared
  m:SetButtonState(m.btn.Save, not isShared, isShared and "Disabled: shared tabs are read-only and cannot be saved.")
  m:SetButtonState(m.btn.Rename, not isShared, isShared and "Disabled: shared tabs are read-only and cannot be renamed.")
  if m.btn.Share then
    m:SetButtonState(m.btn.Share, not isShared, isShared and "Disabled: shared tabs are read-only and cannot be reshared.")
  end
  local pinLabel = self:IsNotePinned(id) and "Unpin from screen" or "Pin to screen"
  if m.btn["Pin to screen"] and m.btn["Pin to screen"].txt then
    m.btn["Pin to screen"].txt:SetText(pinLabel)
  end
  m:SetButtonState(m.btn.Delete, true, nil)
  m:SetButtonState(m.btn["Pin to screen"], true, nil)
  m:SetButtonState(m.btn["Discard all notes to the right"], true, nil)
  m.btn.Save:SetScript("OnClick", function()
    if not m.btn.Save.enabledState then return end
    m:Hide(); self:SelectNote(id); self:ManualSaveSelected()
  end)
  m.btn.Rename:SetScript("OnClick", function()
    if not m.btn.Rename.enabledState then return end
    m:Hide(); self:SelectNote(id); self:RenameSelectedNote()
  end)
  m.btn.Delete:SetScript("OnClick", function()
    if not m.btn.Delete.enabledState then return end
    m:Hide(); self:SelectNote(id); self:DeleteSelectedNote()
  end)
  if m.btn.Share then
    m.btn.Share:SetScript("OnClick", function()
      if not m.btn.Share.enabledState then return end
      m:Hide(); self:SelectNote(id); if self.ShowShareMenu and self.UI and self.UI.shareBtn then self:ShowShareMenu(self.UI.shareBtn) end
    end)
  end
  m.btn["Pin to screen"]:SetScript("OnClick", function()
    if not m.btn["Pin to screen"].enabledState then return end
    m:Hide()
    self:SelectNote(id)
    if self:IsNotePinned(id) then
      self:ClearPinnedNote()
      self:ShowFeedbackMessage("Unpinned note.", { hold = 2.2 })
    else
      self:SetPinnedNote(id)
      local n = self:GetNoteByID(id)
      self:ShowFeedbackMessage("Pinned note: " .. tostring((n and n.title) or "untitled"), { hold = 2.5 })
    end
  end)
  m.btn["Discard all notes to the right"]:SetScript("OnClick", function()
    if not m.btn["Discard all notes to the right"].enabledState then return end
    m:Hide(); self:SelectNote(id); self:CloseTabsToRight(id)
  end)
  local s = UIParent:GetEffectiveScale(); local x,y = GetCursorPosition()
  m:ClearAllPoints(); m:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x/s, y/s); self:ShowContextMenu(m)
end

function AN:RefreshTabs()
  local ui = self.UI
  if not ui or not ui.main or not ui.tabRow or not ui.tabsClip or not ui.addTab then
    return
  end
  local ids = self:GetVisibleNoteIDs(); local total = #ids
  if total > 0 then
    local ok=false for _,id in ipairs(ids) do if id==ui.selectedID then ok=true; break end end
    if not ok then self:SelectNote(ids[1]); return end
  else
    ui.selectedID = nil
  end

  local addW = ui.addTab:GetWidth()
  local minW,maxW = 72,150

  -- First decide if overflow exists using the full tab-row width (no arrows reserved).
  local rowW = math.max(80, ui.tabRow:GetWidth())
  local noArrowW = math.max(80, rowW - addW - 1)
  local maxVisibleNoArrows = math.max(1, math.floor(noArrowW / minW))
  local scroll = total > maxVisibleNoArrows

  -- Then anchor clip region based on whether arrows are needed.
  ui.tabsClip:ClearAllPoints()
  ui.tabsClip:SetPoint("TOPLEFT", 0, 0)
  if scroll then
    ui.tabsClip:SetPoint("BOTTOMRIGHT", ui.tabLeft, "BOTTOMLEFT", -4, 0)
  else
    ui.tabsClip:SetPoint("BOTTOMRIGHT", ui.tabRow, "BOTTOMRIGHT", 0, 0)
  end

  local w = math.max(80, ui.tabsClip:GetWidth() - addW - 1)
  local maxVisible = math.max(1, math.floor(w / minW))
  ui.tabLeft:SetShown(scroll); ui.tabRight:SetShown(scroll)
  ui.tabOffset = scroll and clamp(ui.tabOffset, 1, math.max(1, total-maxVisible+1)) or 1
  local start = ui.tabOffset
  local count = math.min(scroll and maxVisible or math.max(1,total), math.max(0, total-start+1))
  local tabW = clamp(count>0 and math.floor(w/count) or maxW, minW, maxW)

  for i=1,#ui.tabButtons do ui.tabButtons[i]:Hide() end
  for slot=1,count do
    local idx = start + slot - 1; local id = ids[idx]; local n = id and self:GetNoteByID(id)
    if n then
      local b = ui.tabButtons[slot]
      if not b then
        b = CreateFrame("Button", nil, ui.tabsClip)
        b.bg = b:CreateTexture(nil, "BACKGROUND"); b.bg:SetAllPoints(); b.bg:SetColorTexture(0,0,0,0.8); border(b)
        b.txt = b:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        b.txt:SetPoint("LEFT", 6, 0)
        b.txt:SetPoint("RIGHT", -6, 0)
        b.txt:SetJustifyH("LEFT")
        b.txt:SetWordWrap(false)
        if b.txt.SetMaxLines then
          b.txt:SetMaxLines(1)
        end
        b.lock = b:CreateTexture(nil, "ARTWORK")
        b.lock:SetSize(15, 15)
        b.lock:SetPoint("LEFT", 4, 0)
        b.lock:SetTexture(ICON.lock)
        b.lock:SetVertexColor(1, 0.9, 0.2, 0.5)
        b:RegisterForClicks("LeftButtonUp", "RightButtonUp")
        b:SetScript("OnClick", function(selfBtn, btnName)
          if btnName=="LeftButton" then
            AN:SelectNote(selfBtn.id)
          else
            AN:ShowTabContextMenu(selfBtn.id)
          end
        end)
        b:SetScript("OnEnter", function(selfBtn)
          local noteData = selfBtn.note
          if not (noteData and noteData.isShared) then
            return
          end
          local t = mkTip()
          local tipText = ("Shared tab from %s\nRead-only"):format(tostring(noteData.sharedFrom or noteData.createdBy or "Unknown"))
          local PAD = 14
          local MIN_W, MAX_W = 80, 320
          t.txt:SetWidth(0.001)
          t.txt:SetText(tipText)
          local naturalW = math.ceil(t.txt:GetStringWidth() + 0.5)
          local contentW = math.max(MIN_W - PAD, math.min(MAX_W - PAD, naturalW))
          t.txt:SetWidth(contentW)
          local contentH = math.ceil(t.txt:GetStringHeight() + 0.5)
          t:SetSize(contentW + PAD, math.max(24, contentH + PAD))
          PositionTooltipNearOwner(t, selfBtn)
          t:Show()
        end)
        b:SetScript("OnLeave", function()
          if AN.UI.tip then AN.UI.tip:Hide() end
        end)
        ui.tabButtons[slot] = b
      end
      b.note = n
      b.id = id; b:SetSize(tabW-1, 24)
      if slot==1 then b:SetPoint("TOPLEFT", ui.tabsClip, "TOPLEFT", 0,0) else b:SetPoint("LEFT", ui.tabButtons[slot-1], "RIGHT", 1,0) end
      local selected = (id == ui.selectedID)
      local isShared = (n.isShared == true)
      if selected then
        -- Active tab matches textarea black/alpha so it feels connected.
        if isShared then
          b.bg:SetColorTexture(0.12, 0.10, 0.00, 0.82)
        else
          b.bg:SetColorTexture(0, 0, 0, 0.8)
        end
        b.t:SetColorTexture(0, 0, 0, 1)
        b.b:SetColorTexture(0, 0, 0, 0)
        b.l:SetColorTexture(0, 0, 0, 1)
        b.r:SetColorTexture(0, 0, 0, 1)
        b.txt:SetTextColor(1.0, 0.86, 0.20, 1)
      else
        -- Inactive tabs: 40% transparent background and border.
        if isShared then
          b.bg:SetColorTexture(0.10, 0.08, 0.00, 0.62)
        else
          b.bg:SetColorTexture(0, 0, 0, 0.6)
        end
        b.t:SetColorTexture(0, 0, 0, 0.6)
        b.b:SetColorTexture(0, 0, 0, 0.6)
        b.l:SetColorTexture(0, 0, 0, 0.6)
        b.r:SetColorTexture(0, 0, 0, 0.6)
        b.txt:SetTextColor(1, 1, 1, 0.8)
      end
      b.lock:SetShown(isShared)
      b.txt:SetFontObject((tabW<96) and "GameFontNormalSmall" or "GameFontNormal")
      local leftInset
      local rightInset = 6
      if isShared then
        b.txt:ClearAllPoints()
        b.txt:SetPoint("LEFT", b.lock, "RIGHT", 3, 0)
        b.txt:SetPoint("RIGHT", -6, 0)
        -- 4px left inset + lock width + 3px gap between lock and text.
        leftInset = 4 + (b.lock:GetWidth() or 15) + 3
      else
        b.txt:ClearAllPoints()
        b.txt:SetPoint("LEFT", 6, 0)
        b.txt:SetPoint("RIGHT", -6, 0)
        leftInset = 6
      end
      local usable = tabW - (leftInset + rightInset)
      local shownTitle = tostring(n.title or "untitled")
      if (not n.isShared) and (n.userSaved ~= true or n.dirty == true) then
        shownTitle = shownTitle .. "*"
      end
      b.txt:SetText(truncToWidth(b.txt, shownTitle, math.max(10, usable)))
      b:Show()
    end
  end

  local atMax = self:IsAtMaxNotes()
  enableBtn(ui.addTab, not atMax)
  if ui.newBtn then
    enableBtn(ui.newBtn, not atMax)
    ui.newBtn.tip = atMax and ("Maximum notes reached ("..(self.DB.settings.maxNotes or 100)..").") or "Create a new note"
  end
  ui.addTab.tip = atMax and ("Maximum notes reached ("..(self.DB.settings.maxNotes or 100)..").") or "Create a new note"
  ui.addTab:ClearAllPoints()
  if count > 0 and ui.tabButtons[count] and ui.tabButtons[count]:IsShown() then
    ui.addTab:SetPoint("LEFT", ui.tabButtons[count], "RIGHT", 0, 0)
  else
    ui.addTab:SetPoint("TOPLEFT", ui.tabsClip, "TOPLEFT", 0, 0)
  end
end

function AN:RefreshAllUI()
  if not self.UI.main then return end
  self:RefreshFolderPanel(); self:RefreshTabs(); self:RefreshURLState(); self:RefreshActiveNoteMetadata(); self:RefreshNoteActionStates()
  if self.RefreshPinnedNoteFrame then
    self:RefreshPinnedNoteFrame()
  end
end

function AN:IsInEditModeActive()
  return EditModeManagerFrame and EditModeManagerFrame:IsEditModeActive() or false
end

function AN:SavePinnedFramePosition()
  local p = self:GetPinnedSettings()
  local ui = self.UI
  local f = ui and ui.pinned
  if not (p and f) then return end
  local point, _, _, x, y = f:GetPoint(1)
  p.point = point or "CENTER"
  p.x = tonumber(x) or 0
  p.y = tonumber(y) or 0
end

function AN:ApplyPinnedFrameSettings()
  local p = self:GetPinnedSettings()
  local ui = self.UI
  local f = ui and ui.pinned
  if not (p and f) then return end

  f:SetSize(tonumber(p.width) or 360, tonumber(p.height) or 260)
  f:SetScale(tonumber(p.scale) or 1.0)
  if f.bg then
    f.bg:SetAlpha(tonumber(p.parchmentAlpha) or 1.0)
  end

  local ba = tonumber(p.borderAlpha) or 0.4
  if f.inner and f.inner.t then f.inner.t:SetColorTexture(0, 0, 0, ba) end
  if f.inner and f.inner.b then f.inner.b:SetColorTexture(0, 0, 0, ba) end
  if f.inner and f.inner.l then f.inner.l:SetColorTexture(0, 0, 0, ba) end
  if f.inner and f.inner.r then f.inner.r:SetColorTexture(0, 0, 0, ba) end

  if f.UpdateTextMetrics then
    f:UpdateTextMetrics()
  end
end

function AN:CreatePinnedEditOptionsPanel()
  local ui = self.UI
  if not ui then return nil end
  if ui.pinnedOptions then return ui.pinnedOptions end

  local f = flat(UIParent, 0.9)
  f:SetSize(430, 390)
  f:SetFrameStrata("FULLSCREEN_DIALOG")
  f:SetToplevel(true)
  f:SetMovable(true)
  f:SetClampedToScreen(true)
  f:EnableMouse(true)
  f:RegisterForDrag("LeftButton")
  f:SetScript("OnDragStart", f.StartMoving)
  f:SetScript("OnDragStop", function(selfFrame)
    selfFrame:StopMovingOrSizing()
    ClampFrameToScreenAbsolute(selfFrame, 19)
  end)
  f:Hide()

  do
    local ok, borderFrame = pcall(CreateFrame, "Frame", nil, f, "DialogBorderTranslucentTemplate")
    if ok and borderFrame then
      borderFrame:SetPoint("TOPLEFT", f, "TOPLEFT", -8, 8)
      borderFrame:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 8, -8)
      f.blizzBorder = borderFrame
    end
  end

  f.title = f:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
  f.title:SetPoint("TOPLEFT", 10, -10)
  f.title:SetText("Pinned Note Settings")

  f.close = CreateFrame("Button", nil, f, "UIPanelCloseButtonNoScripts")
  f.close:SetPoint("CENTER", f, "TOPRIGHT", -4, -4)
  f.close:SetScript("OnClick", function() f:Hide() end)

  local function attachBlizzardTooltip(widget, title, body)
    if not widget then return end
    widget:SetScript("OnEnter", function(selfWidget)
      GameTooltip:SetOwner(selfWidget, "ANCHOR_RIGHT")
      GameTooltip:SetText(title or "")
      if body and body ~= "" then
        GameTooltip:AddLine(body, 0.85, 0.85, 0.85, true)
      end
      GameTooltip:Show()
    end)
    widget:SetScript("OnLeave", function()
      GameTooltip:Hide()
    end)
  end

  local function isChildOf(child, parentFrame)
    local cur = child
    while cur do
      if cur == parentFrame then
        return true
      end
      if not cur.GetParent then
        break
      end
      cur = cur:GetParent()
    end
    return false
  end

  f:SetScript("OnShow", function(selfFrame)
    selfFrame:RegisterEvent("GLOBAL_MOUSE_DOWN")
  end)
  f:SetScript("OnHide", function(selfFrame)
    selfFrame:UnregisterEvent("GLOBAL_MOUSE_DOWN")
  end)
  f:SetScript("OnEvent", function(selfFrame, event, button)
    if event ~= "GLOBAL_MOUSE_DOWN" then return end
    if button and button ~= "LeftButton" then return end
    if not (AN and AN.IsInEditModeActive and AN:IsInEditModeActive()) then return end
    if selfFrame:IsMouseOver() then return end

    local focus = (GetMouseFocus and GetMouseFocus()) or nil
    if focus and isChildOf(focus, selfFrame) then
      return
    end

    if AN and AN.HidePinnedEditOptions then
      AN:HidePinnedEditOptions(true)
    else
      selfFrame:Hide()
    end
  end)

  local y = -34
  local function createSlider(key, label, minV, maxV, step, formatFunc, onChanged)
    local row = CreateFrame("Frame", nil, f)
    row:SetPoint("TOPLEFT", 12, y)
    row:SetPoint("TOPRIGHT", -12, y)
    row:SetHeight(44)

    row.lbl = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    row.lbl:SetPoint("TOPLEFT", 0, 0)
    row.lbl:SetText(label)

    local sName = "AmmeNotepad_PinnedOptSlider_" .. key
    row.slider = CreateFrame("Slider", sName, row, "OptionsSliderTemplate")
    row.slider:SetPoint("TOPLEFT", row, "TOPLEFT", 0, -18)
    row.slider:SetWidth(320)
    row.slider:SetMinMaxValues(minV, maxV)
    row.slider:SetValueStep(step or 1)
    row.slider:SetObeyStepOnDrag(true)
    _G[sName .. "Low"]:SetText("")
    _G[sName .. "High"]:SetText("")
    _G[sName .. "Text"]:SetText("")

    row.value = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.value:SetPoint("LEFT", row.slider, "RIGHT", 8, 0)
    row.value:SetWidth(46)
    row.value:SetJustifyH("RIGHT")

    row.slider:SetScript("OnValueChanged", function(_, v)
      if f._block then return end
      if formatFunc then
        row.value:SetText(formatFunc(v))
      else
        row.value:SetText(tostring(math.floor(v + 0.5)))
      end
      if onChanged then
        onChanged(v)
      end
    end)

    y = y - 46
    return row
  end

  f.widthRow = createSlider("Width", "Note Width", 180, 1100, 10, function(v) return tostring(math.floor(v + 0.5)) end, function(v)
    local p = AN:GetPinnedSettings()
    if not p then return end
    p.width = clamp(math.floor((tonumber(v) or 360) + 0.5), 180, 1100)
    AN.SessionPendingDiskSave = true
    AN:ApplyPinnedFrameSettings()
    if AN.UI and AN.UI.pinned then
      ClampFrameToScreen(AN.UI.pinned)
      AN:SavePinnedFramePosition()
    end
  end)
  attachBlizzardTooltip(f.widthRow.slider, "Note Width", "Sets the width of the pinned note frame.")

  f.heightRow = createSlider("Height", "Note Height", 100, 1200, 10, function(v) return tostring(math.floor(v + 0.5)) end, function(v)
    local p = AN:GetPinnedSettings()
    if not p then return end
    p.height = clamp(math.floor((tonumber(v) or 260) + 0.5), 100, 1200)
    AN.SessionPendingDiskSave = true
    AN:ApplyPinnedFrameSettings()
    if AN.UI and AN.UI.pinned then
      ClampFrameToScreen(AN.UI.pinned)
      AN:SavePinnedFramePosition()
    end
  end)
  attachBlizzardTooltip(f.heightRow.slider, "Note Height", "Sets the height of the pinned note frame.")

  f.scaleRow = createSlider("Scale", "Note Scale", 0.5, 2.0, 0.1, function(v) return string.format("%.1f", v) end, function(v)
    local p = AN:GetPinnedSettings()
    if not p then return end
    p.scale = clamp(math.floor(((tonumber(v) or 1.0) * 10) + 0.5) / 10, 0.5, 2.0)
    AN.SessionPendingDiskSave = true
    AN:ApplyPinnedFrameSettings()
    if AN.UI and AN.UI.pinned then
      ClampFrameToScreen(AN.UI.pinned)
      AN:SavePinnedFramePosition()
    end
  end)
  attachBlizzardTooltip(f.scaleRow.slider, "Note Scale", "Scales the pinned note without affecting the main addon UI scale.")

  f.lockCheck = CreateFrame("CheckButton", nil, f, "ChatConfigCheckButtonTemplate")
  f.lockCheck:SetPoint("TOPLEFT", 12, y - 2)
  f.lockCheck.Text:SetText("Note Position Locked")
  f.lockCheck:SetScript("OnClick", function(selfCheck)
    local p = AN:GetPinnedSettings()
    if not p then return end
    p.locked = selfCheck:GetChecked() and true or false
    AN.SessionPendingDiskSave = true
    AN:RefreshPinnedNoteFrame()
  end)
  attachBlizzardTooltip(f.lockCheck, "Note Position Locked", "When enabled, the pinned note cannot be moved or resized outside Edit Mode.")
  y = y - 42

  f.titleCheck = CreateFrame("CheckButton", nil, f, "ChatConfigCheckButtonTemplate")
  f.titleCheck:SetPoint("TOPLEFT", 12, y - 2)
  f.titleCheck.Text:SetText("Show Note Title in Pin")
  f.titleCheck:SetScript("OnClick", function(selfCheck)
    local p = AN:GetPinnedSettings()
    if not p then return end
    p.showTitle = selfCheck:GetChecked() and true or false
    AN.SessionPendingDiskSave = true
    AN:RefreshPinnedNoteFrame()
  end)
  attachBlizzardTooltip(f.titleCheck, "Show Note Title in Pin", "Shows the note title above the note body in the pinned frame.")
  y = y - 36

  f.hideScrollCheck = CreateFrame("CheckButton", nil, f, "ChatConfigCheckButtonTemplate")
  f.hideScrollCheck:SetPoint("TOPLEFT", 12, y - 2)
  f.hideScrollCheck.Text:SetText("Hide Pinned Note Scrollbar")
  f.hideScrollCheck:SetScript("OnClick", function(selfCheck)
    local p = AN:GetPinnedSettings()
    if not p then return end
    p.hideScrollbar = selfCheck:GetChecked() and true or false
    AN.SessionPendingDiskSave = true
    AN:UpdatePinnedScrollbarVisibility()
  end)
  attachBlizzardTooltip(f.hideScrollCheck, "Hide Pinned Note Scrollbar", "When enabled, the pinned scrollbar only appears while the mouse is over the pinned note.")
  y = y - 36

  f.parchmentRow = createSlider("Parchment", "Background Parchment Opacity", 0, 100, 1, function(v) return string.format("%d%%", math.floor(v + 0.5)) end, function(v)
    local p = AN:GetPinnedSettings()
    if not p then return end
    p.parchmentAlpha = clamp((tonumber(v) or 100) / 100, 0, 1)
    AN.SessionPendingDiskSave = true
    AN:ApplyPinnedFrameSettings()
  end)
  attachBlizzardTooltip(f.parchmentRow.slider, "Background Parchment Opacity", "Adjusts parchment background opacity for the pinned note.")

  f.borderRow = createSlider("Border", "Border Opacity", 0, 100, 1, function(v) return string.format("%d%%", math.floor(v + 0.5)) end, function(v)
    local p = AN:GetPinnedSettings()
    if not p then return end
    p.borderAlpha = clamp((tonumber(v) or 40) / 100, 0, 1)
    AN.SessionPendingDiskSave = true
    AN:ApplyPinnedFrameSettings()
  end)
  attachBlizzardTooltip(f.borderRow.slider, "Border Opacity", "Adjusts opacity of the pinned note border.")

  f.SyncFromSettings = function(selfFrame)
    local p = AN:GetPinnedSettings()
    if not p then return end
    selfFrame._block = true
    selfFrame.widthRow.slider:SetValue(tonumber(p.width) or 360)
    selfFrame.widthRow.value:SetText(tostring(math.floor((tonumber(p.width) or 360) + 0.5)))
    selfFrame.heightRow.slider:SetValue(tonumber(p.height) or 260)
    selfFrame.heightRow.value:SetText(tostring(math.floor((tonumber(p.height) or 260) + 0.5)))
    selfFrame.scaleRow.slider:SetValue(tonumber(p.scale) or 1.0)
    selfFrame.scaleRow.value:SetText(string.format("%.1f", tonumber(p.scale) or 1.0))
    selfFrame.lockCheck:SetChecked(p.locked ~= false)
    selfFrame.titleCheck:SetChecked(p.showTitle ~= false)
    selfFrame.hideScrollCheck:SetChecked(p.hideScrollbar ~= false)
    selfFrame.parchmentRow.slider:SetValue((tonumber(p.parchmentAlpha) or 1.0) * 100)
    selfFrame.parchmentRow.value:SetText(string.format("%d%%", math.floor(((tonumber(p.parchmentAlpha) or 1.0) * 100) + 0.5)))
    selfFrame.borderRow.slider:SetValue((tonumber(p.borderAlpha) or 0.4) * 100)
    selfFrame.borderRow.value:SetText(string.format("%d%%", math.floor(((tonumber(p.borderAlpha) or 0.4) * 100) + 0.5)))
    selfFrame._block = false
  end

  ui.pinnedOptions = f
  return f
end

function AN:ShowPinnedEditOptions(show)
  local ui = self.UI
  local f = self:CreatePinnedEditOptionsPanel()
  if not (ui and f) then return end
  if show == false then
    f:Hide()
    return
  end
  if not (ui.pinned and ui.pinned:IsShown() and self:IsInEditModeActive()) then
    f:Hide()
    return
  end
  if f.SyncFromSettings then
    f:SyncFromSettings()
  end
  f:ClearAllPoints()
  f:SetPoint("LEFT", ui.pinned, "RIGHT", 10, 0)
  f:Show()
  ClampFrameToScreenAbsolute(f, 19)
  C_Timer.After(0, function()
    if f and f:IsShown() then
      ClampFrameToScreenAbsolute(f, 19)
    end
  end)
end

function AN:TogglePinnedEditOptions()
  local ui = self.UI
  if not ui then return end
  if ui.pinnedOptions and ui.pinnedOptions:IsShown() then
    self:HidePinnedEditOptions(true)
  else
    self:ShowPinnedEditOptions(true)
  end
end

function AN:ResetPinnedSelectionState()
  local ui = self.UI
  local sel = ui and ui.pinned and ui.pinned.selection
  if not sel then return end
  sel.isSelected = false
  if sel.ShowHighlighted then
    pcall(sel.ShowHighlighted, sel)
  end
  if sel.Label and sel.Label.Hide then
    sel.Label:Hide()
  end
  if sel.label and sel.label.Hide then
    sel.label:Hide()
  end
  if sel.MouseOverHighlight and not sel:IsMouseOver() then
    sel.MouseOverHighlight:Hide()
  end
end

function AN:HidePinnedEditOptions(resetSelection)
  local ui = self.UI
  if ui and ui.pinnedOptions and ui.pinnedOptions:IsShown() then
    ui.pinnedOptions:Hide()
  end
  if resetSelection then
    self:ResetPinnedSelectionState()
  end
end

function AN:ClosePinnedContextMenu()
  local ui = self.UI
  if ui and ui.pinnedMenu and ui.pinnedMenu:IsShown() then
    ui.pinnedMenu:Hide()
  end
end

function AN:OpenMainWindowForPinnedNote()
  local p = self:GetPinnedSettings()
  if not p then return end
  local noteID = tostring(p.noteID or "")
  if noteID == "" then return end
  local note = self:GetNoteByID(noteID)
  if not note then return end

  local ui = self.UI
  if not (ui and ui.main) and self.CreateMainWindow then
    self:CreateMainWindow()
    ui = self.UI
  end
  if not (ui and ui.main) then return end

  if not ui.main:IsShown() then
    self:ApplyUIScale(self.DB and self.DB.settings and self.DB.settings.uiScale)
    ui.main:Show()
    self:RestartAutosaveTicker()
  end

  if note.folder and self.SelectFolder then
    self:SelectFolder(note.folder)
  end
  if self.SelectNote then
    self:SelectNote(noteID)
  end
  if self.RefreshAllUI then
    self:RefreshAllUI()
  end
end

function AN:UpdatePinnedScrollbarVisibility()
  local ui = self.UI
  local f = ui and ui.pinned
  local p = self:GetPinnedSettings()
  if not (f and p and f.scroll and f.scrollBar) then return end

  if self:IsInEditModeActive() then
    f.scrollBar:Hide()
    return
  end

  if p.hideScrollbar ~= false then
    local inside = false
    if MouseIsOver then
      inside = MouseIsOver(f)
      if (not inside) and f.scrollBar then
        inside = MouseIsOver(f.scrollBar)
      end
    end
    f.scrollBar:SetShown(inside)
  else
    f.scrollBar:Show()
  end
end

function AN:DismissPinnedMenuOnLockedLeftClick()
  local p = self:GetPinnedSettings()
  if not p then return end
  if p.locked ~= false then
    self:ClosePinnedContextMenu()
  end
end

function AN:ShowPinnedContextMenu()
  if self:IsInEditModeActive() then
    return
  end

  local ui = self.UI
  if not ui then return end
  if not ui.pinnedMenu then
    local m = flat(UIParent, 0.9)
    m:SetFrameStrata("FULLSCREEN_DIALOG")
    m:SetSize(210, 64)
    m:SetToplevel(true)
    m:SetClampedToScreen(true)
    m:EnableMouse(true)
    m:Hide()

    local function makeRow(topOffset)
      local b = CreateFrame("Button", nil, m)
      b:SetPoint("TOPLEFT", 6, topOffset)
      b:SetPoint("TOPRIGHT", -6, topOffset)
      b:SetHeight(24)
      b.bg = b:CreateTexture(nil, "BACKGROUND")
      b.bg:SetAllPoints()
      b.bg:SetColorTexture(0, 0, 0, 0.75)
      border(b)
      b.txt = b:CreateFontString(nil, "OVERLAY", "GameFontNormal")
      b.txt:SetPoint("LEFT", 8, 0)
      b.txt:SetPoint("RIGHT", -6, 0)
      b.txt:SetJustifyH("LEFT")
      b:SetScript("OnEnter", function(selfBtn)
        selfBtn.bg:SetColorTexture(0.12, 0.12, 0.12, 0.9)
      end)
      b:SetScript("OnLeave", function(selfBtn)
        selfBtn.bg:SetColorTexture(0, 0, 0, 0.75)
      end)
      return b
    end

    m.btnEdit = makeRow(-6)
    m.btnLock = makeRow(-34)

    ui.pinnedMenu = m
    self:RegisterContextMenu(m)
  end

  local m = ui.pinnedMenu
  local p = self:GetPinnedSettings()
  if not p then return end
  if m.btnEdit and m.btnEdit.txt then
    m.btnEdit.txt:SetText("Edit pinned note")
  end
  if m.btnLock and m.btnLock.txt then
    m.btnLock.txt:SetText((p.locked ~= false) and "Unlock pinned note" or "Lock pinned note")
  end
  if m.btnEdit then
    m.btnEdit:SetScript("OnClick", function()
      m:Hide()
      AN:OpenMainWindowForPinnedNote()
    end)
  end
  if m.btnLock then
    m.btnLock:SetScript("OnClick", function()
      local pin = AN:GetPinnedSettings()
      if not pin then return end
      pin.locked = not (pin.locked ~= false)
      AN.SessionPendingDiskSave = true
      if AN.UI and AN.UI.pinnedOptions and AN.UI.pinnedOptions:IsShown() and AN.UI.pinnedOptions.SyncFromSettings then
        AN.UI.pinnedOptions:SyncFromSettings()
      end
      AN:RefreshPinnedNoteFrame()
      m:Hide()
    end)
  end

  local s = UIParent:GetEffectiveScale()
  local x, y = GetCursorPosition()
  m:ClearAllPoints()
  m:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x / s, y / s)
  self:ShowContextMenu(m)
end

function AN:CreatePinnedNoteFrame()
  local ui = self.UI
  if not ui then return nil end
  if ui.pinned then return ui.pinned end

  local f = CreateFrame("Frame", "AmmeNotepad_PinnedNoteFrame", UIParent)
  f:SetFrameStrata("MEDIUM")
  f:SetToplevel(true)
  f:SetMovable(true)
  f:SetClampedToScreen(true)
  f:EnableMouse(true)
  f:RegisterForDrag("LeftButton")
  f:SetScript("OnDragStart", function(selfFrame)
    local p = AN:GetPinnedSettings()
    if not p then return end
    if p.locked and not AN:IsInEditModeActive() then
      return
    end
    selfFrame:StartMoving()
  end)
  f:SetScript("OnDragStop", function(selfFrame)
    selfFrame:StopMovingOrSizing()
    ClampFrameToScreen(selfFrame)
    AN:SavePinnedFramePosition()
  end)

  f.bg = f:CreateTexture(nil, "BACKGROUND")
  f.bg:SetAllPoints()
  f.bg:SetTexture("Interface\\AddOns\\AmmeNotepad\\Art\\ui_partchment.png")
  f.bg:SetTexCoord(0, 1, 1, 1, 0, 0, 1, 0)

  f.inner = CreateFrame("Frame", nil, f)
  f.inner:SetPoint("TOPLEFT", 11, -11)
  f.inner:SetPoint("BOTTOMRIGHT", -11, 11)
  border(f.inner)
  if f.inner.t then f.inner.t:SetHeight(2) end
  if f.inner.b then f.inner.b:SetHeight(2) end
  if f.inner.l then f.inner.l:SetWidth(2) end
  if f.inner.r then f.inner.r:SetWidth(2) end

  f.pad = CreateFrame("Frame", nil, f.inner)
  f.pad:SetPoint("TOPLEFT", 12, -12)
  f.pad:SetPoint("BOTTOMRIGHT", -12, 12)

  f.scroll = CreateFrame("ScrollFrame", "AmmeNotepad_PinnedNoteScrollFrame", f.pad, "UIPanelScrollFrameTemplate")
  f.scroll:SetPoint("TOPLEFT", 0, 0)
  f.scroll:SetPoint("BOTTOMRIGHT", -15, -7)
  f.scrollBar = f.scroll.ScrollBar or (f.scroll.GetName and _G[f.scroll:GetName() .. "ScrollBar"])
  local syncingPinnedScroll = false
  f.SyncPinnedScrollBar = function(selfFrame)
    local sf = selfFrame and selfFrame.scroll or f.scroll
    local sb = selfFrame and selfFrame.scrollBar or f.scrollBar
    if not (sf and sb) then return end
    local range = sf:GetVerticalScrollRange() or 0
    if range < 0 then range = 0 end
    sb:SetMinMaxValues(0, range)
    local offset = clamp(tonumber(sf:GetVerticalScroll()) or 0, 0, range)
    syncingPinnedScroll = true
    sb:SetValue(offset)
    syncingPinnedScroll = false
  end
  if f.scrollBar then
    f.scrollBar:HookScript("OnMouseDown", function(_, button)
      if button == "LeftButton" then
        AN:DismissPinnedMenuOnLockedLeftClick()
      end
    end)
    f.scrollBar:SetScript("OnValueChanged", function(_, value)
      if syncingPinnedScroll then return end
      local range = f.scroll:GetVerticalScrollRange() or 0
      local target = clamp(tonumber(value) or 0, 0, range)
      f.scroll:SetVerticalScroll(target)
      if f.SyncPinnedScrollBar then
        f:SyncPinnedScrollBar()
      end
    end)
  end

  f.child = CreateFrame("Frame", nil, f.scroll)
  f.child:SetSize(120, 120)
  f.scroll:SetScrollChild(f.child)

  f.titleText = f.child:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge")
  f.titleText:SetPoint("TOPLEFT", 0, 0)
  f.titleText:SetJustifyH("LEFT")
  f.titleText:SetJustifyV("TOP")
  f.titleText:SetWordWrap(false)
  if f.titleText.SetMaxLines then
    f.titleText:SetMaxLines(1)
  end
  f.titleText:SetText("")
  f.titleText:Hide()

  f.text = f.child:CreateFontString(nil, "ARTWORK", "GameFontNormal")
  f.text:SetPoint("TOPLEFT", 0, 0)
  f.text:SetJustifyH("LEFT")
  f.text:SetJustifyV("TOP")
  f.text:SetWordWrap(true)
  f.text:SetText("")

  f.UpdateTextMetrics = function(selfFrame)
    local width = math.max(40, (selfFrame.scroll:GetWidth() or 40) - 2)
    if selfFrame.titleText then
      selfFrame.titleText:SetWidth(width)
    end
    selfFrame.text:SetWidth(width)
    local titleH = 0
    if selfFrame.titleText and selfFrame.titleText:IsShown() then
      titleH = math.ceil((selfFrame.titleText:GetStringHeight() or 0) + 2)
    end
    local textH = math.ceil((selfFrame.text:GetStringHeight() or 0) + 4)
    local viewH = math.ceil(selfFrame.scroll:GetHeight() or 0)
    selfFrame.child:SetHeight(math.max(viewH, titleH + textH))
    if selfFrame.SyncPinnedScrollBar then
      selfFrame:SyncPinnedScrollBar()
    end
  end
  f.scroll:HookScript("OnVerticalScroll", function(selfScroll, offset)
    local range = selfScroll:GetVerticalScrollRange() or 0
    local clamped = clamp(tonumber(offset) or 0, 0, range)
    if math.abs((selfScroll:GetVerticalScroll() or 0) - clamped) > 0.1 then
      selfScroll:SetVerticalScroll(clamped)
    end
    if f and f.SyncPinnedScrollBar then
      f:SyncPinnedScrollBar()
    end
  end)
  f.scroll:HookScript("OnSizeChanged", function()
    if f and f.UpdateTextMetrics then
      f:UpdateTextMetrics()
    end
  end)
  f.scroll:SetScript("OnMouseDown", function(_, button)
    if button == "LeftButton" then
      AN:DismissPinnedMenuOnLockedLeftClick()
    end
  end)
  f.scroll:SetScript("OnMouseUp", function(_, button)
    if button == "RightButton" then
      AN:ShowPinnedContextMenu()
    end
  end)

  f.grip = CreateFrame("Button", nil, f.inner)
  f.grip:SetSize(16, 16)
  f.grip:SetPoint("BOTTOMRIGHT", 8, -8)
  f.grip:EnableMouse(true)
  f.grip:RegisterForDrag("LeftButton")
  f.grip.icon = f.grip:CreateTexture(nil, "ARTWORK")
  f.grip.icon:SetAllPoints()
  f.grip.icon:SetTexture(ICON.resize)
  f.grip.icon:SetAlpha(0.5)
  local function finalizePinnedResize(selfBtn)
    f._resizing = false
    selfBtn:SetScript("OnUpdate", nil)
    local p = AN:GetPinnedSettings()
    if p then
      p.width = clamp(math.floor((f:GetWidth() or 360) + 0.5), 180, 1100)
      p.height = clamp(math.floor((f:GetHeight() or 260) + 0.5), 100, 1200)
    end
    AN.SessionPendingDiskSave = true
    ClampFrameToScreen(f)
    AN:SavePinnedFramePosition()
    if AN.UI and AN.UI.pinnedOptions and AN.UI.pinnedOptions:IsShown() and AN.UI.pinnedOptions.SyncFromSettings then
      AN.UI.pinnedOptions:SyncFromSettings()
    end
    if f.UpdateTextMetrics then
      f:UpdateTextMetrics()
    end
  end
  f.grip:SetScript("OnMouseDown", function(selfBtn)
    local p = AN:GetPinnedSettings()
    if not p or (p.locked ~= false) then
      return
    end
    local s = f:GetEffectiveScale()
    local cx, cy = GetCursorPosition()
    f._resizeStartX = cx / s
    f._resizeStartY = cy / s
    f._resizeStartW = f:GetWidth()
    f._resizeStartH = f:GetHeight()
    f._resizeLeft = f:GetLeft()
    f._resizeTop = f:GetTop()
    f._resizing = true
    selfBtn:SetScript("OnUpdate", function()
      if not f._resizing then return end
      if IsMouseButtonDown and (not IsMouseButtonDown("LeftButton")) then
        finalizePinnedResize(selfBtn)
        return
      end
      local cx2, cy2 = GetCursorPosition()
      cx2 = cx2 / s
      cy2 = cy2 / s
      local newW = clamp((f._resizeStartW or f:GetWidth()) + (cx2 - (f._resizeStartX or cx2)), 180, 1100)
      local newH = clamp((f._resizeStartH or f:GetHeight()) + ((f._resizeStartY or cy2) - cy2), 100, 1200)
      if f._resizeLeft and f._resizeTop then
        f:ClearAllPoints()
        f:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", f._resizeLeft, f._resizeTop)
      end
      f:SetSize(newW, newH)
      if f.UpdateTextMetrics then
        f:UpdateTextMetrics()
      end
    end)
  end)
  f.grip:SetScript("OnMouseUp", function(selfBtn)
    finalizePinnedResize(selfBtn)
  end)
  f.grip:SetScript("OnLeave", function(selfBtn)
    if f._resizing and IsMouseButtonDown and (not IsMouseButtonDown("LeftButton")) then
      finalizePinnedResize(selfBtn)
    end
  end)

  local selectionOK, selectionFrame = pcall(CreateFrame, "Frame", nil, f, "EditModeSystemSelectionTemplate")
  if not (selectionOK and selectionFrame) then
    selectionFrame = CreateFrame("Button", nil, f)
  end
  f.selection = selectionFrame
  f.selection:SetPoint("TOPLEFT", -2, 2)
  f.selection:SetPoint("BOTTOMRIGHT", 2, -2)
  f.selection:SetFrameStrata("DIALOG")
  f.selection:SetFrameLevel(f:GetFrameLevel() + 60)
  f.selection:EnableMouse(true)
  f.selection:RegisterForDrag("LeftButton")
  if selectionOK and selectionFrame then
    f.selection.parent = f
    f.selection.overlayHidden = false
    f.selection.system = f.selection.system or {}
    if type(f.selection.system.GetSystemName) ~= "function" then
      f.selection.system.GetSystemName = function()
        return "Pinned Note"
      end
    end
    if f.selection.Label and f.selection.Label.SetText then
      f.selection.Label:SetText("Pinned Note")
    end
    if f.selection.label and f.selection.label.SetText then
      f.selection.label:SetText("Pinned Note")
    end
    if f.selection.MouseOverHighlight then
      f.selection.MouseOverHighlight:SetPoint("TOPLEFT", f.selection, "TOPLEFT", 0, 0)
      f.selection.MouseOverHighlight:SetPoint("BOTTOMRIGHT", f.selection, "BOTTOMRIGHT", 0, 0)
      f.selection.MouseOverHighlight:Hide()
    end
    f.selection:HookScript("OnLeave", function(selfSel)
      if not (AN and AN.IsInEditModeActive and AN:IsInEditModeActive()) then return end
      if selfSel.ShowHighlighted and not selfSel.isSelected then
        pcall(selfSel.ShowHighlighted, selfSel)
      end
    end)
  else
    f.selection.bg = f.selection:CreateTexture(nil, "BACKGROUND", nil, -8)
    f.selection.bg:SetAllPoints()
    f.selection.bg:SetColorTexture(0.22, 0.52, 0.95, 0.24)
    border(f.selection)
    f.selection.label = f.selection:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    f.selection.label:SetPoint("CENTER")
    f.selection.label:SetText("Pinned Note")
  end
  f.selection:SetScript("OnDragStart", function()
    f:StartMoving()
  end)
  f.selection:SetScript("OnDragStop", function()
    f:StopMovingOrSizing()
    ClampFrameToScreen(f)
    AN:SavePinnedFramePosition()
  end)
  f.selection:SetScript("OnMouseDown", function(_, button)
    if button == "LeftButton" then
      if EditModeManagerFrame and EditModeManagerFrame.ClearSelectedSystem then
        EditModeManagerFrame:ClearSelectedSystem()
      end
      if f.selection.ShowSelected then
        pcall(f.selection.ShowSelected, f.selection, true)
        f.selection.isSelected = true
      elseif f.selection.ShowHighlighted then
        pcall(f.selection.ShowHighlighted, f.selection)
      end
      AN:TogglePinnedEditOptions()
    elseif button == "RightButton" then
      AN:ShowPinnedContextMenu()
    end
  end)
  f.selection:Hide()

  f:SetScript("OnMouseUp", function(_, button)
    if button == "RightButton" then
      AN:ShowPinnedContextMenu()
    end
  end)
  f:SetScript("OnMouseDown", function(_, button)
    if button == "LeftButton" then
      AN:DismissPinnedMenuOnLockedLeftClick()
    end
  end)
  f:SetScript("OnUpdate", function(selfFrame, elapsed)
    selfFrame._sbTick = (selfFrame._sbTick or 0) + (tonumber(elapsed) or 0)
    if selfFrame._sbTick >= 0.08 then
      selfFrame._sbTick = 0
      AN:UpdatePinnedScrollbarVisibility()
    end
  end)

  f:Hide()
  ui.pinned = f
  return f
end

function AN:RefreshPinnedNoteFrame()
  local p = self:GetPinnedSettings()
  local ui = self.UI
  if not (p and ui) then return end
  local f = self:CreatePinnedNoteFrame()
  if not f then return end

  local id = tostring(p.noteID or "")
  local note = (id ~= "") and self:GetNoteByID(id) or nil
  if not note then
    f:Hide()
    self:HidePinnedEditOptions()
    return
  end

  if not f._placed then
    f:ClearAllPoints()
    f:SetPoint(tostring(p.point or "CENTER"), UIParent, tostring(p.point or "CENTER"), tonumber(p.x) or 0, tonumber(p.y) or 0)
    ClampFrameToScreen(f)
    self:SavePinnedFramePosition()
    f._placed = true
  end

  self:ApplyPinnedFrameSettings()

  local appearance = self:GetNoteAppearance(note)
  local fontPath = self:ResolveFontPath(appearance.fontName)
  local fontSize = tonumber(appearance.fontSize) or 12
  local c = appearance.fontColor or { r = 0.95, g = 0.95, b = 0.95 }
  local showTitle = (p.showTitle ~= false)

  if f.pad then
    local topInset = showTitle and 4 or 12
    if f._padTopInset ~= topInset then
      f._padTopInset = topInset
      f.pad:ClearAllPoints()
      f.pad:SetPoint("TOPLEFT", 12, -topInset)
      f.pad:SetPoint("BOTTOMRIGHT", -12, 12)
    end
  end

  local applied = false
  if f.text.SetFont then
    applied = pcall(f.text.SetFont, f.text, fontPath, fontSize, "")
  end
  if (not applied) and f.text.SetFont then
    pcall(f.text.SetFont, f.text, "Fonts\\FRIZQT__.TTF", fontSize, "")
  end
  f.text:SetTextColor(c.r, c.g, c.b, 1)
  if f.titleText then
    local titleSize = clamp((tonumber(fontSize) or 12) + 2, 10, 32)
    local titleApplied = false
    if f.titleText.SetFont then
      titleApplied = pcall(f.titleText.SetFont, f.titleText, fontPath, titleSize, "")
    end
    if (not titleApplied) and f.titleText.SetFont then
      pcall(f.titleText.SetFont, f.titleText, "Fonts\\FRIZQT__.TTF", titleSize, "")
    end
    f.titleText:SetTextColor(1, 1, 1, 1)
    f.titleText:SetText(tostring(note.title or "untitled"))
    f.titleText:SetShown(showTitle)
    f.text:ClearAllPoints()
    if showTitle then
      f.text:SetPoint("TOPLEFT", f.titleText, "BOTTOMLEFT", 0, -2)
    else
      f.text:SetPoint("TOPLEFT", 0, 0)
    end
  end
  f.text:SetText(tostring(note.body or ""))
  if f.UpdateTextMetrics then
    f:UpdateTextMetrics()
  end

  f.selection:SetShown(self:IsInEditModeActive())
  if f.selection:IsShown() then
    if f.selection.ShowHighlighted and not f.selection.isSelected then
      pcall(f.selection.ShowHighlighted, f.selection)
    end
    if f.selection.MouseOverHighlight and not f.selection:IsMouseOver() then
      f.selection.MouseOverHighlight:Hide()
    end
  end
  if f.grip then
    f.grip:SetShown((p.locked == false))
  end
  self:UpdatePinnedScrollbarVisibility()
  if not self:IsInEditModeActive() then
    self:HidePinnedEditOptions()
  end

  f:Show()
end

function AN:PinSelectedNote()
  local ui = self.UI
  local id = ui and ui.selectedID
  if not id then return end
  local note = self:GetNoteByID(id)
  if not note then return end
  local ok = self:SetPinnedNote(id)
  if ok then
    self:ShowFeedbackMessage("Pinned note: " .. tostring(note.title or "untitled"), { hold = 2.5 })
  end
end

function AN:TogglePinSelectedNote()
  local ui = self.UI
  local id = ui and ui.selectedID
  if not id then return end
  if self:IsNotePinned(id) then
    self:ClearPinnedNote()
    self:ShowFeedbackMessage("Unpinned note.", { hold = 2.2 })
  else
    self:PinSelectedNote()
  end
end

function AN:OnEditModeChanged(active)
  local ui = self.UI
  if not ui then return end
  ui.inEditMode = active and true or false
  if ui.inEditMode and ui.pinnedMenu and ui.pinnedMenu:IsShown() then
    ui.pinnedMenu:Hide()
  end
  self:RefreshPinnedNoteFrame()
  if not ui.inEditMode then
    self:HidePinnedEditOptions()
  end
end

function AN:SetupEditModeHooks()
  if self._PinnedEditModeHooked then
    return
  end

  if EditModeManagerFrame and (not self._PinnedEditModeSelectHooked) and hooksecurefunc then
    hooksecurefunc(EditModeManagerFrame, "SelectSystem", function()
      if AN and AN.HidePinnedEditOptions then
        AN:HidePinnedEditOptions(true)
      end
    end)
    self._PinnedEditModeSelectHooked = true
  end

  if EventRegistry and EventRegistry.RegisterCallback then
    EventRegistry:RegisterCallback("EditMode.Enter", function()
      if AN and AN.OnEditModeChanged then
        AN:OnEditModeChanged(true)
      end
    end)
    EventRegistry:RegisterCallback("EditMode.Exit", function()
      if AN and AN.OnEditModeChanged then
        AN:OnEditModeChanged(false)
      end
    end)
    self._PinnedEditModeHooked = true
  else
    self._PinnedEditModeHooked = true
    self._PinnedEditModeTicker = C_Timer.NewTicker(0.25, function()
      if not AN or not AN.OnEditModeChanged then return end
      local state = AN:IsInEditModeActive()
      if AN._PinnedEditModeLast ~= state then
        AN._PinnedEditModeLast = state
        AN:OnEditModeChanged(state)
      end
    end)
  end
end

function AN:InitializePinnedNote()
  self:CreatePinnedNoteFrame()
  self:RefreshPinnedNoteFrame()
end

function AN:ApplyUIScale(scale)
  if not (self and self.DB and self.DB.settings) then
    return 0.7
  end

  local s = clamp(tonumber(scale) or tonumber(self.DB.settings.uiScale) or 0.7, 0.5, 1.8)
  s = math.floor((s * 10) + 0.5) / 10
  self.DB.settings.uiScale = s

  local ui = self.UI
  if not ui then
    return s
  end

  if ui.main and ui.main.SetScale then
    ui.main:SetScale(s)
  end
  if ui.folderPanel and ui.folderPanel.SetScale then
    ui.folderPanel:SetScale(s)
  end

  -- Re-apply side-panel anchoring after scale changes so its dual anchors
  -- (TOPLEFT/BOTTOMLEFT) remain intact and it stays attached to the main frame.
  if ui.folderPanel and self.SetFolderPanelExpanded then
    self:SetFolderPanelExpanded(ui.folderPanelExpanded == true, false)
  end

  return s
end

function AN:RestartAutosaveTicker()
  local ui = self.UI
  if ui.autoTicker then ui.autoTicker:Cancel(); ui.autoTicker = nil end
  if not self.DB.settings.autosaveEnabled then return end
  local sec = (tonumber(self.DB.settings.autosaveIntervalMins) or 5) * 60
  ui.autoTicker = C_Timer.NewTicker(sec, function()
    if not (ui.main and ui.main:IsShown() and ui.selectedID and ui.edit) then return end
    local id = ui.selectedID
    local body = ui.edit:GetText() or ""
    ui.lastAutoSnapshot = ui.lastAutoSnapshot or {}
    if ui.lastAutoSnapshot[id] == body then
      return
    end
    if self:SaveCurrentNote(true) then
      ui.lastAutoSnapshot[id] = body
      self:UpdateSaveStatus("Autosaved at " .. date("%H:%M:%S"))
    end
  end)
end

function AN:ToggleMainWindow()
  if not self.UI.main then self:CreateMainWindow() end
  if self.UI.main:IsShown() then
    self:RequestHideMainWindow()
  else
    self:ApplyUIScale(self.DB and self.DB.settings and self.DB.settings.uiScale)
    self.UI.main:Show(); self:RefreshAllUI(); self:RestartAutosaveTicker()
  end
end

function AN:RequestHideMainWindow()
  local ui = self.UI
  if not ui.main or not ui.main:IsShown() then return end

  if self.RefreshSessionWarningState then
    self:RefreshSessionWarningState()
  end

  if not self.SessionPendingDiskSave then
    ui.main:Hide()
    return
  end

  if self.SessionSuppressDiskSaveWarning then
    ui.main:Hide()
    return
  end

  local msg = "You have notes saved in this session that are not yet written permanently to SavedVariables on disk.\n\nReload UI now to commit them, or close anyway and risk losing them if the game crashes before logout/reload."
  local function applyReminderChoice(dontRemind)
    if dontRemind then
      self.SessionSuppressDiskSaveWarning = true
    end
  end
  self:ShowConfirmDialog(
    "Session Save Warning",
    msg,
    function(dontRemind)
      applyReminderChoice(dontRemind)
      if ReloadUI then
        ReloadUI()
      else
        ui.main:Hide()
      end
    end,
    function(dontRemind)
      applyReminderChoice(dontRemind)
      ui.main:Hide()
    end,
    "Reload UI",
    "Close Anyway",
    {
      checkboxLabel = "Don't remind me again this session",
      checkboxDefault = true,
    }
  )
end

function AN:CreateMainWindow()
  local ui = self.UI
  if ui.main then return end
  local win = self.DB.settings.window or {}

  local f = CreateFrame("Frame", "AmmeNotepad_MainFrame", UIParent)
  f:SetFrameStrata("MEDIUM"); f:SetMovable(true); f:EnableMouse(true); f:SetClampedToScreen(true)
  f:EnableKeyboard(true)
  if f.SetPropagateKeyboardInput then
    f:SetPropagateKeyboardInput(true)
  end
  f:SetScript("OnKeyDown", function(selfFrame, key)
    if selfFrame.SetPropagateKeyboardInput then
      selfFrame:SetPropagateKeyboardInput(true)
    end
    if key ~= "ESCAPE" then
      return
    end
    if IsAnyModalShown(ui) then
      return
    end
    if selfFrame.SetPropagateKeyboardInput then
      selfFrame:SetPropagateKeyboardInput(false)
    end
    AN:RequestHideMainWindow()
  end)
  local function stopMoveAndSave(selfFrame)
    selfFrame:StopMovingOrSizing()
    local p,_,_,x,y = selfFrame:GetPoint(1)
    local w = AN.DB.settings.window; w.point = p or "CENTER"; w.x = x or 0; w.y = y or 0
  end
  f:SetSize(clamp(tonumber(win.width) or 520, 420, 680), clamp(tonumber(win.height) or 760, 620, 980))
  if win.point then f:SetPoint(win.point, UIParent, win.point, tonumber(win.x) or 0, tonumber(win.y) or 0) else f:SetPoint("CENTER") end

  f.bg = f:CreateTexture(nil, "BACKGROUND"); f.bg:SetAllPoints(); f.bg:SetTexture("Interface\\AddOns\\AmmeNotepad\\Art\\ui_partchment.png")
  -- Rotate parchment 90deg without geometric overflow.
  f.bg:SetTexCoord(0, 1, 1, 1, 0, 0, 1, 0)

  f.titleIcon = f:CreateTexture(nil, "OVERLAY")
  f.titleIcon:SetSize(27, 27)
  f.titleIcon:SetPoint("TOPLEFT", 16, -12)
  f.titleIcon:SetTexture("Interface\\AddOns\\AmmeNotepad\\Art\\quill.png")
  f.title = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
  f.title:SetPoint("LEFT", f.titleIcon, "RIGHT", 6, 0)
  f.title:SetText((AN.META.Title or "AmmeNotepad") .. " v" .. (AN.META.Version or "?"))
  f.close = btn(f,22,ICON.del,"Hide notepad"); f.close:SetPoint("TOPRIGHT", -12, -12); f.close:SetScript("OnClick", function() AN:RequestHideMainWindow() end)
  setGlyph(f.close, "X", "GameFontHighlight", 1, 0.25, 0.25, 1)

  -- Restrict whole-window dragging to header only.
  f.dragger = CreateFrame("Frame", nil, f)
  f.dragger:SetPoint("TOPLEFT", 8, -6)
  f.dragger:SetPoint("TOPRIGHT", -42, -6)
  f.dragger:SetHeight(30)
  f.dragger:EnableMouse(true)
  f.dragger:RegisterForDrag("LeftButton")
  f.dragger:SetScript("OnDragStart", function() f:StartMoving() end)
  f.dragger:SetScript("OnDragStop", function() stopMoveAndSave(f) end)

  -- Collapsible folders panel (sits behind the main window).
  ui.folderPanelWidth = 244
  ui.folderPanelPeek = 20
  ui.folderPanelJoinOffset = 7
  local fp = CreateFrame("Frame", "AmmeNotepad_FolderPanel", UIParent)
  fp:SetFrameStrata("LOW")
  fp:SetFrameLevel(2)
  fp:SetWidth(ui.folderPanelWidth)
  fp:SetPoint("TOPLEFT", f, "TOPLEFT", -ui.folderPanelPeek, 0)
  fp:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", -ui.folderPanelPeek, 0)
  fp:SetClampedToScreen(true)
  fp:EnableMouse(true)
  fp.bg = fp:CreateTexture(nil, "BACKGROUND")
  fp.bg:SetAllPoints()
  fp.bg:SetTexture("Interface\\AddOns\\AmmeNotepad\\Art\\ui_partchment.png")
  fp.bg:SetTexCoord(0, 1, 1, 1, 0, 0, 1, 0)

  ui.folderPanel = fp
  ui.folderPanelExpanded = (self.DB.settings.folderPanelExpanded == true)
  ui.folderPanelOffset = ui.folderPanelExpanded and (-(ui.folderPanelWidth - ui.folderPanelJoinOffset)) or (-ui.folderPanelPeek)

  ui.folderPanelContent = CreateFrame("Frame", nil, fp)
  ui.folderPanelContent:SetPoint("TOPLEFT", 10, -8)
  ui.folderPanelContent:SetPoint("TOPRIGHT", -2, -8)
  ui.folderPanelContent:SetPoint("BOTTOMLEFT", 10, 8)
  ui.folderPanelContent:SetPoint("BOTTOMRIGHT", -2, 8)

  local folderLayoutYOffset = -27
  ui.folderPanelMode = "folders"

  ui.folderTabsRow = CreateFrame("Frame", nil, ui.folderPanelContent)
  ui.folderTabsRow:SetPoint("TOPLEFT", 0, folderLayoutYOffset)
  ui.folderTabsRow:SetPoint("TOPRIGHT", -30, folderLayoutYOffset)
  ui.folderTabsRow:SetHeight(24)

  local function createSideTab(parent, label)
    local b = CreateFrame("Button", nil, parent)
    b.bg = b:CreateTexture(nil, "BACKGROUND")
    b.bg:SetAllPoints()
    b.bg:SetColorTexture(0, 0, 0, 0.6)
    border(b)
    b.txt = b:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    b.txt:SetPoint("LEFT", 6, 0)
    b.txt:SetPoint("RIGHT", -6, 0)
    b.txt:SetJustifyH("LEFT")
    b.txt:SetText(label or "")
    return b
  end

  ui.folderModeTab = createSideTab(ui.folderTabsRow, "Folders")
  ui.folderModeTab:SetPoint("TOPLEFT", 0, 0)
  ui.folderModeTab:SetPoint("BOTTOMLEFT", 0, 0)
  ui.folderModeTab:SetWidth(95)
  ui.groupModeTab = createSideTab(ui.folderTabsRow, "Group")
  ui.groupModeTab:SetPoint("TOPLEFT", ui.folderModeTab, "TOPRIGHT", 1, 0)
  ui.groupModeTab:SetPoint("BOTTOMLEFT", ui.folderModeTab, "BOTTOMRIGHT", 1, 0)
  ui.groupModeTab:SetPoint("RIGHT", 0, 0)

  function ui:UpdateFolderModeTabs()
    local function styleTab(tab, active)
      if not tab then return end
      if active then
        tab.bg:SetColorTexture(0, 0, 0, 0.8)
        if tab.t then tab.t:SetColorTexture(0, 0, 0, 1) end
        if tab.b then tab.b:SetColorTexture(0, 0, 0, 0) end
        if tab.l then tab.l:SetColorTexture(0, 0, 0, 1) end
        if tab.r then tab.r:SetColorTexture(0, 0, 0, 1) end
        tab.txt:SetTextColor(1.0, 0.86, 0.20, 1)
      else
        tab.bg:SetColorTexture(0, 0, 0, 0.6)
        if tab.t then tab.t:SetColorTexture(0, 0, 0, 0.6) end
        if tab.b then tab.b:SetColorTexture(0, 0, 0, 0.6) end
        if tab.l then tab.l:SetColorTexture(0, 0, 0, 0.6) end
        if tab.r then tab.r:SetColorTexture(0, 0, 0, 0.6) end
        tab.txt:SetTextColor(1, 1, 1, 0.82)
      end
    end
    local mode = (self.folderPanelMode == "group") and "group" or "folders"
    styleTab(self.folderModeTab, mode == "folders")
    styleTab(self.groupModeTab, mode == "group")
  end

  ui.folderModeTab:SetScript("OnClick", function()
    if AN.ClosePanelMenusForSideActions then
      AN:ClosePanelMenusForSideActions()
    end
    AN:SetFolderPanelMode("folders")
  end)
  ui.groupModeTab:SetScript("OnClick", function()
    if AN.ClosePanelMenusForSideActions then
      AN:ClosePanelMenusForSideActions()
    end
    AN:SetFolderPanelMode("group")
  end)
  ui:UpdateFolderModeTabs()

  local folderBtnRow = CreateFrame("Frame", nil, ui.folderPanelContent)
  folderBtnRow:SetPoint("TOPLEFT", ui.folderTabsRow, "BOTTOMLEFT", 0, -6)
  folderBtnRow:SetPoint("TOPRIGHT", ui.folderTabsRow, "BOTTOMRIGHT", 0, -6)
  folderBtnRow:SetHeight(22)
  ui.folderBtnRow = folderBtnRow

  local function setFolderIconBtnState(button, enabled, disabledTip)
    if not button then return end
    button.enabledState = enabled and true or false
    button.disabledTip = disabledTip
    button:SetEnabled(button.enabledState)
    if button.icon then
      button.icon:SetDesaturated(not button.enabledState)
      button.icon:SetVertexColor(button.enabledState and 1 or 0.55, button.enabledState and 1 or 0.55, button.enabledState and 1 or 0.55, 1)
    end
    if button.actionIcon then
      button.actionIcon:SetDesaturated(not button.enabledState)
      button.actionIcon:SetVertexColor(button.enabledState and 1 or 0.55, button.enabledState and 1 or 0.55, button.enabledState and 1 or 0.55, 1)
    end
    if button.bg then
      button.bg:SetColorTexture(0, 0, 0, button.enabledState and 0.8 or 0.6)
    end
    if button.enabledState then
      button.tip = button.baseTip or ""
    else
      local extra = tostring(disabledTip or "Disabled.")
      if button.baseTip and button.baseTip ~= "" then
        button.tip = button.baseTip .. "\n\n" .. extra
      else
        button.tip = extra
      end
    end
  end

  local function applyFolderActionComposite(button, actionTex)
    if not button then return end
    if button.icon then
      button.icon:SetTexture(ICON.folder)
      button.icon:SetPoint("TOPLEFT", 2, -2)
      button.icon:SetPoint("BOTTOMRIGHT", -2, 2)
    end
    if not button.actionIcon then
      button.actionIcon = button:CreateTexture(nil, "OVERLAY")
    end
    button.actionIcon:SetTexture(actionTex)
    local side = math.max(8, math.floor((button:GetWidth() or 20) * 0.5 + 0.5))
    button.actionIcon:SetSize(side, side)
    button.actionIcon:ClearAllPoints()
    button.actionIcon:SetPoint("BOTTOMRIGHT", -2, 2)
  end

  ui.folderAddBtn = btn(folderBtnRow, 20, ICON.folder, "Create folder")
  ui.folderAddBtn.baseTip = "Create folder"
  ui.folderAddBtn:SetPoint("LEFT", 0, 0)
  ui.folderAddBtn:SetScript("OnClick", function() AN:CreateFolderPrompt() end)
  applyFolderActionComposite(ui.folderAddBtn, ICON.plus)
  if ui.folderAddBtn.SetMotionScriptsWhileDisabled then ui.folderAddBtn:SetMotionScriptsWhileDisabled(true) end
  setFolderIconBtnState(ui.folderAddBtn, true, nil)

  ui.folderRenameBtn = btn(folderBtnRow, 20, ICON.folder, "Rename selected folder")
  ui.folderRenameBtn.baseTip = "Rename selected folder"
  ui.folderRenameBtn:SetPoint("LEFT", ui.folderAddBtn, "RIGHT", 3, 0)
  ui.folderRenameBtn:SetScript("OnClick", function() AN:RenameSelectedFolderPrompt() end)
  applyFolderActionComposite(ui.folderRenameBtn, ICON.rename)
  if ui.folderRenameBtn.SetMotionScriptsWhileDisabled then ui.folderRenameBtn:SetMotionScriptsWhileDisabled(true) end
  setFolderIconBtnState(ui.folderRenameBtn, true, nil)

  ui.folderDeleteBtn = btn(folderBtnRow, 20, ICON.folder, "Delete selected folder")
  ui.folderDeleteBtn.baseTip = "Delete selected folder"
  ui.folderDeleteBtn:SetPoint("LEFT", ui.folderRenameBtn, "RIGHT", 3, 0)
  ui.folderDeleteBtn:SetScript("OnClick", function(selfBtn)
    if selfBtn.enabledState == false then return end
    AN:DeleteSelectedFolderPrompt()
  end)
  applyFolderActionComposite(ui.folderDeleteBtn, ICON.del)
  if ui.folderDeleteBtn.SetMotionScriptsWhileDisabled then ui.folderDeleteBtn:SetMotionScriptsWhileDisabled(true) end
  setFolderIconBtnState(ui.folderDeleteBtn, true, nil)

  ui.folderListShell = flat(ui.folderPanelContent, 0.8)
  ui.folderListShell:SetPoint("TOPLEFT", folderBtnRow, "BOTTOMLEFT", 0, -6)
  ui.folderListShell:SetPoint("TOPRIGHT", folderBtnRow, "BOTTOMRIGHT", 0, -6)
  ui.folderListShell:SetPoint("BOTTOMLEFT", ui.folderPanelContent, "BOTTOMLEFT", 0, 44)
  ui.folderListShell:SetPoint("BOTTOMRIGHT", ui.folderPanelContent, "BOTTOMRIGHT", -30, 44)
  if ui.folderListShell.SetClipsChildren then
    ui.folderListShell:SetClipsChildren(true)
  end

  ui.folderScroll = CreateFrame("ScrollFrame", "AmmeNotepad_FolderScrollFrame", ui.folderPanelContent, "UIPanelScrollFrameTemplate")
  ui.folderScroll:SetPoint("TOPLEFT", ui.folderListShell, "TOPLEFT", 4, -4)
  ui.folderScroll:SetPoint("BOTTOMRIGHT", ui.folderListShell, "BOTTOMRIGHT", -4, 4)
  local folderSB = ui.folderScroll.ScrollBar or (ui.folderScroll.GetName and _G[ui.folderScroll:GetName() .. "ScrollBar"])
  if folderSB then
    folderSB:ClearAllPoints()
    folderSB:SetPoint("TOPLEFT", ui.folderListShell, "TOPRIGHT", 2, -16)
    folderSB:SetPoint("BOTTOMLEFT", ui.folderListShell, "BOTTOMRIGHT", 2, 16)
    ui.folderScroll.ScrollBar = folderSB
  end
  ui.folderScroll:EnableMouseWheel(true)
  ui.folderScroll:SetScript("OnMouseWheel", function(selfScroll, delta)
    local step = 22
    local range = selfScroll:GetVerticalScrollRange() or 0
    local newOffset = (selfScroll:GetVerticalScroll() or 0) - (delta * step)
    if newOffset < 0 then newOffset = 0 end
    if newOffset > range then newOffset = range end
    selfScroll:SetVerticalScroll(newOffset)
  end)

  ui.folderListContent = CreateFrame("Frame", nil, ui.folderScroll)
  ui.folderListContent:SetSize(120, 120)
  ui.folderScroll:SetScrollChild(ui.folderListContent)

  ui.groupEmptyText = ui.folderListShell:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  ui.groupEmptyText:SetPoint("TOPLEFT", 8, -8)
  ui.groupEmptyText:SetPoint("TOPRIGHT", -8, -8)
  ui.groupEmptyText:SetPoint("BOTTOMLEFT", 8, 8)
  ui.groupEmptyText:SetPoint("BOTTOMRIGHT", -8, 8)
  ui.groupEmptyText:SetJustifyH("CENTER")
  ui.groupEmptyText:SetJustifyV("MIDDLE")
  ui.groupEmptyText:SetWordWrap(true)
  ui.groupEmptyText:SetTextColor(0.72, 0.72, 0.72, 0.9)
  ui.groupEmptyText:SetText("You are not currenty in a group")
  ui.groupEmptyText:Hide()

  ui.folderStatsLine1 = ui.folderPanelContent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  ui.folderStatsLine1:SetPoint("TOPLEFT", ui.folderListShell, "BOTTOMLEFT", 2, -6)
  ui.folderStatsLine1:SetPoint("TOPRIGHT", ui.folderPanelContent, "TOPRIGHT", -2, -6)
  ui.folderStatsLine1:SetJustifyH("LEFT")
  ui.folderStatsLine1:SetTextColor(0.95, 0.95, 0.95, 0.9)
  ui.folderStatsLine1:SetText("0 notes across 0 folders.")

  ui.folderStatsLine2 = ui.folderPanelContent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  ui.folderStatsLine2:SetPoint("TOPLEFT", ui.folderStatsLine1, "BOTTOMLEFT", 0, -2)
  ui.folderStatsLine2:SetPoint("TOPRIGHT", ui.folderStatsLine1, "BOTTOMRIGHT", 0, -2)
  ui.folderStatsLine2:SetJustifyH("LEFT")
  ui.folderStatsLine2:SetTextColor(0.90, 0.90, 0.90, 0.9)
  ui.folderStatsLine2:SetText("0 unsaved notes")

  ui.groupHelpText = ui.folderPanelContent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  ui.groupHelpText:SetPoint("TOPLEFT", ui.folderListShell, "BOTTOMLEFT", 2, -6)
  ui.groupHelpText:SetPoint("TOPRIGHT", ui.folderPanelContent, "TOPRIGHT", -2, -6)
  ui.groupHelpText:SetJustifyH("LEFT")
  ui.groupHelpText:SetJustifyV("TOP")
  ui.groupHelpText:SetWordWrap(true)
  ui.groupHelpText:SetTextColor(0.92, 0.92, 0.92, 0.9)
  ui.groupHelpText:SetText("Click on a group member to add their name to your current note. Shift click them to include their server name.")
  ui.groupHelpText:Hide()

  ui.folderToggle = CreateFrame("Button", nil, fp)
  ui.folderToggle:SetSize(14, 30)
  ui.folderToggle.bg = ui.folderToggle:CreateTexture(nil, "BACKGROUND")
  ui.folderToggle.bg:SetAllPoints()
  ui.folderToggle.bg:SetColorTexture(0, 0, 0, 0.8)
  border(ui.folderToggle)
  ui.folderToggle.icon = ui.folderToggle:CreateTexture(nil, "ARTWORK")
  ui.folderToggle.icon:SetPoint("TOPLEFT", 1, -1)
  ui.folderToggle.icon:SetPoint("BOTTOMRIGHT", -1, 1)
  ui.folderToggle.icon:SetTexture(ICON.chevLeft)
  ui.folderToggle.tip = "Expand folders panel"
  ui.folderToggle:SetScript("OnEnter", function(selfBtn)
    selfBtn.bg:SetColorTexture(0.12, 0.12, 0.12, 0.9)
    local t = mkTip()
    local text = selfBtn.tip or ""
    local PAD = 14
    local MIN_W, MAX_W = 80, 320
    t.txt:SetWidth(0.001)
    t.txt:SetText(text)
    local naturalW = math.ceil(t.txt:GetStringWidth() + 0.5)
    local contentW = math.max(MIN_W - PAD, math.min(MAX_W - PAD, naturalW))
    t.txt:SetWidth(contentW)
    local contentH = math.ceil(t.txt:GetStringHeight() + 0.5)
    t:SetSize(contentW + PAD, math.max(24, contentH + PAD))
    PositionTooltipNearOwner(t, selfBtn)
    t:Show()
  end)
  ui.folderToggle:SetScript("OnLeave", function(selfBtn)
    selfBtn.bg:SetColorTexture(0, 0, 0, 0.8)
    if AN.UI.tip then AN.UI.tip:Hide() end
  end)
  ui.folderToggle:SetScript("OnClick", function()
    AN:SetFolderPanelExpanded(not ui.folderPanelExpanded, true)
  end)

  local row = CreateFrame("Frame", nil, f); row:SetPoint("TOPLEFT", 12, -42); row:SetPoint("TOPRIGHT", -12, -42); row:SetHeight(28)
  ui.newBtn = btn(row,24,ICON.plus,"Create a new note")
  ui.newBtn:SetPoint("LEFT", 0, 0)
  ui.newBtn:SetScript("OnClick", function()
    if AN:IsAtMaxNotes() then return end
    local id = AN:CreateNote(nil)
    if id then AN:SelectNote(id); AN:RefreshTabs() end
  end)
  ui.saveBtn = btn(row,24,ICON.save,"Save current note"); ui.saveBtn:SetPoint("LEFT",ui.newBtn, "RIGHT", 4,0); ui.saveBtn:SetScript("OnClick", function() AN:ManualSaveSelected() end)
  ui.targetBtn = btn(row,24,ICON.target,"Insert current target name"); ui.targetBtn:SetPoint("LEFT", ui.saveBtn, "RIGHT", 4,0); ui.targetBtn:SetScript("OnClick", function() AN:InsertTargetName() end)
  if ui.targetBtn.SetMotionScriptsWhileDisabled then
    ui.targetBtn:SetMotionScriptsWhileDisabled(true)
  end
  ui.delBtn = btn(row,24,ICON.del,"Delete current note"); ui.delBtn:SetPoint("LEFT", ui.targetBtn, "RIGHT", 4,0); ui.delBtn:SetScript("OnClick", function() AN:DeleteSelectedNote() end)
  ui.renameBtn = btn(row,24,ICON.rename,"Rename current note"); ui.renameBtn:SetPoint("LEFT", ui.delBtn, "RIGHT", 4,0); ui.renameBtn:SetScript("OnClick", function() AN:RenameSelectedNote() end)
  ui.undoBtn = btn(row,24,ICON.undo,"Undo"); ui.undoBtn:SetPoint("LEFT", ui.renameBtn, "RIGHT", 4,0); ui.undoBtn:SetScript("OnClick", function() AN:Undo() end)
  ui.redoBtn = btn(row,24,ICON.redo,"Redo"); ui.redoBtn:SetPoint("LEFT", ui.undoBtn, "RIGHT", 4,0); ui.redoBtn:SetScript("OnClick", function() AN:Redo() end)
  ui.shareBtn = btn(row,24,ICON.share,"Share note"); ui.shareBtn:SetPoint("LEFT", ui.redoBtn, "RIGHT", 4, 0); ui.shareBtn:SetScript("OnClick", function(selfBtn) AN:ShowShareMenu(selfBtn) end)
  ui.pinBtn = btn(row,24,ICON.pin,"Pin this note to screen")
  ui.pinBtn:SetPoint("LEFT", ui.shareBtn, "RIGHT", 4, 0)
  ui.pinBtn:SetScript("OnClick", function() AN:TogglePinSelectedNote() end)

  ui.searchShell = flat(row,0.8); ui.searchShell:SetPoint("TOPRIGHT",0,0); ui.searchShell:SetSize(230,24)
  ui.search = CreateFrame("EditBox", nil, ui.searchShell); ui.search:SetAutoFocus(false); ui.search:SetFontObject("GameFontNormalSmall"); ui.search:SetPoint("TOPLEFT",6,-4); ui.search:SetPoint("BOTTOMRIGHT",-22,4)
  ui.searchPlaceholder = ui.searchShell:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  ui.searchPlaceholder:SetPoint("LEFT", 6, 0)
  ui.searchPlaceholder:SetText("search")
  ui.searchPlaceholder:SetTextColor(0.7, 0.7, 0.7, 0.4)
  ui.searchClear = btn(ui.searchShell,12,ICON.del,"Clear search"); ui.searchClear:SetPoint("RIGHT", -4, 0)
  setGlyph(ui.searchClear, "X", "GameFontNormalSmall", 0.9, 0.9, 0.9, 0.9)
  ui.searchClear:SetScript("OnClick", function() ui.search:SetText(""); ui.searchResults = {}; ui.searchDrop:Hide(); AN:UpdateSaveStatus("") end)
  ui.searchClear:Hide()

  ui.modeRow = CreateFrame("Frame", nil, row); ui.modeRow:SetPoint("BOTTOMRIGHT", ui.searchShell, "TOPRIGHT", 0, 4); ui.modeRow:SetSize(230,20); ui.modeRow:Hide()
  local function radio(parent, text)
    local r = CreateFrame("CheckButton", nil, parent); r:SetSize(16,16)
    r.bg = r:CreateTexture(nil, "BACKGROUND"); r.bg:SetAllPoints(); r.bg:SetColorTexture(0,0,0,0.85); border(r)
    r.dot = r:CreateTexture(nil, "ARTWORK"); r.dot:SetPoint("TOPLEFT",4,-4); r.dot:SetPoint("BOTTOMRIGHT",-4,4); r.dot:SetColorTexture(0.95,0.95,0.95,1)
    r.lbl = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall"); r.lbl:SetPoint("LEFT", r, "RIGHT", 4, 0); r.lbl:SetText(text)
    return r
  end
  ui.modeName = radio(ui.modeRow, "Names"); ui.modeName:SetPoint("LEFT",0,0)
  ui.modeContent = radio(ui.modeRow, "Content"); ui.modeContent:SetPoint("LEFT",90,0)
  local function setMode(m)
    AN.DB.settings.searchMode = m; ui.modeName.dot:SetShown(m=="name"); ui.modeContent.dot:SetShown(m=="content")
    local q = sanitizeSearch(ui.search:GetText()); AN:BuildSearchResults(q); AN:RefreshSearchDropdown(); AN:ApplySearchHighlightToCurrentNote()
  end
  ui.modeName:SetScript("OnClick", function() setMode("name") end); ui.modeContent:SetScript("OnClick", function() setMode("content") end)

  ui.searchDrop = flat(row, 0.9); ui.searchDrop:SetPoint("TOPRIGHT", ui.searchShell, "BOTTOMRIGHT", 0, -2); ui.searchDrop:SetWidth(280); ui.searchDrop:SetHeight(10); ui.searchDrop:Hide()
  ui.urlBtn = btn(row,24,ICON.rename,"Detected URLs"); ui.urlBtn:SetPoint("RIGHT", ui.searchShell, "LEFT", -4, 0); ui.urlBtn:SetScript("OnClick", function() AN:ShowURLListMenu() end); ui.urlBtn:Hide()

  ui.feedback = CreateFrame("Frame", nil, f)
  ui.feedback:SetPoint("TOPLEFT", ui.searchShell, "BOTTOMLEFT", 0, -2)
  ui.feedback:SetPoint("TOPRIGHT", ui.searchShell, "BOTTOMRIGHT", 0, -2)
  ui.feedback:SetHeight(14)
  ui.feedback:SetAlpha(0)
  ui.feedback:Hide()
  ui.feedbackText = ui.feedback:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  ui.feedbackText:SetAllPoints()
  ui.feedbackText:SetJustifyH("RIGHT")
  ui.feedbackText:SetJustifyV("MIDDLE")
  ui.feedbackText:SetTextColor(1, 1, 1, 1)
  ui.saveStatus = ui.feedbackText

  local tabRow = CreateFrame("Frame", nil, f); tabRow:SetPoint("TOPLEFT", 16, -83); tabRow:SetPoint("TOPRIGHT", -40, -83); tabRow:SetHeight(26)
  ui.tabRow = tabRow
  ui.addTab = btn(tabRow,20,ICON.plus,"Create a new note")
  ui.addTab:SetScript("OnClick", function() if AN:IsAtMaxNotes() then return end local id=AN:CreateNote(nil); if id then AN:SelectNote(id); AN:RefreshTabs() end end)
  ui.tabRight = btn(tabRow,20,ICON.tabRight,"Scroll tabs right"); ui.tabRight:SetPoint("RIGHT", tabRow, "RIGHT", 0, 0); ui.tabRight:SetScript("OnClick", function() ui.tabOffset = ui.tabOffset + 1; AN:RefreshTabs() end)
  ui.tabLeft = btn(tabRow,20,ICON.tabLeft,"Scroll tabs left"); ui.tabLeft:SetPoint("RIGHT", ui.tabRight, "LEFT", -3, 0); ui.tabLeft:SetScript("OnClick", function() ui.tabOffset = math.max(1, ui.tabOffset - 1); AN:RefreshTabs() end)
  ui.tabsClip = CreateFrame("Frame", nil, tabRow); ui.tabsClip:SetPoint("TOPLEFT",0,0); ui.tabsClip:SetPoint("BOTTOMRIGHT", ui.tabLeft, "BOTTOMLEFT", -4, 0)

  ui.editorOuter = CreateFrame("Frame", nil, f); ui.editorOuter:SetPoint("TOPLEFT", 12, -101); ui.editorOuter:SetPoint("TOPRIGHT", -12, -101); ui.editorOuter:SetPoint("BOTTOMLEFT", 12, 40); ui.editorOuter:SetPoint("BOTTOMRIGHT", -12, 40)

  ui.metaFrame = CreateFrame("Frame", nil, f)
  ui.metaFrame:SetPoint("BOTTOMLEFT", 17, 13)
  ui.metaFrame:SetPoint("BOTTOMRIGHT", -39, 13)
  ui.metaFrame:SetHeight(35)
  border(ui.metaFrame)
  if ui.metaFrame.t then ui.metaFrame.t:SetColorTexture(0, 0, 0, 0.3) end
  if ui.metaFrame.b then ui.metaFrame.b:SetColorTexture(0, 0, 0, 0.3) end
  if ui.metaFrame.l then ui.metaFrame.l:SetColorTexture(0, 0, 0, 0.3) end
  if ui.metaFrame.r then ui.metaFrame.r:SetColorTexture(0, 0, 0, 0.3) end
  ui.metaCreated = ui.metaFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  ui.metaCreated:SetPoint("TOPLEFT", 7, -7)
  ui.metaCreated:SetPoint("TOPRIGHT", -7, -7)
  ui.metaCreated:SetJustifyH("LEFT")
  ui.metaCreated:SetTextColor(0.95, 0.95, 0.95, 0.95)
  ui.metaUpdated = ui.metaFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  ui.metaUpdated:SetPoint("TOPLEFT", ui.metaCreated, "BOTTOMLEFT", 0, -2)
  ui.metaUpdated:SetPoint("TOPRIGHT", ui.metaCreated, "BOTTOMRIGHT", 0, -2)
  ui.metaUpdated:SetJustifyH("LEFT")
  ui.metaUpdated:SetTextColor(0.90, 0.90, 0.90, 0.90)
  local inner = CreateFrame("Frame", nil, ui.editorOuter)
  inner:SetPoint("TOPLEFT", 4, -4)
  inner:SetPoint("BOTTOMRIGHT", -4, 14) -- leave room for grip/scrollbar down arrow

  local textShell = flat(inner, 0.8)
  textShell:SetPoint("TOPLEFT", 0, 0)
  textShell:SetPoint("BOTTOMRIGHT", -24, 0) -- reserve right lane for scrollbar
  ui.textShell = textShell

  local appearanceRow = CreateFrame("Frame", nil, textShell)
  appearanceRow:SetPoint("TOPLEFT", 4, -4)
  appearanceRow:SetPoint("TOPRIGHT", -4, -4)
  appearanceRow:SetHeight(22)
  ui.appearanceRow = appearanceRow

  local function bindTip(selfBtn)
    selfBtn:SetScript("OnEnter", function(owner)
      owner.bg:SetColorTexture(0.12, 0.12, 0.12, 0.9)
      local t = mkTip()
      local text = owner.tip or ""
      local PAD = 14
      local MIN_W, MAX_W = 80, 320
      t.txt:SetWidth(0.001)
      t.txt:SetText(text)
      local naturalW = math.ceil(t.txt:GetStringWidth() + 0.5)
      local contentW = math.max(MIN_W - PAD, math.min(MAX_W - PAD, naturalW))
      t.txt:SetWidth(contentW)
      local contentH = math.ceil(t.txt:GetStringHeight() + 0.5)
      t:SetSize(contentW + PAD, math.max(24, contentH + PAD))
      PositionTooltipNearOwner(t, owner)
      t:Show()
    end)
    selfBtn:SetScript("OnLeave", function(owner)
      owner.bg:SetColorTexture(0, 0, 0, 0.8)
      if AN.UI.tip then AN.UI.tip:Hide() end
    end)
  end

  ui.fontBtn = CreateFrame("Button", nil, appearanceRow)
  ui.fontBtn:SetSize(180, 20)
  ui.fontBtn:SetPoint("LEFT", 0, 0)
  ui.fontBtn.bg = ui.fontBtn:CreateTexture(nil, "BACKGROUND")
  ui.fontBtn.bg:SetAllPoints()
  ui.fontBtn.bg:SetColorTexture(0, 0, 0, 0.8)
  border(ui.fontBtn)
  ui.fontBtn.txt = ui.fontBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  ui.fontBtn.txt:SetPoint("LEFT", 6, 0)
  ui.fontBtn.txt:SetPoint("RIGHT", -6, 0)
  ui.fontBtn.txt:SetJustifyH("LEFT")
  ui.fontBtn:SetScript("OnClick", function(selfBtn)
    if selfBtn.enabledState == false then return end
    AN:ShowFontMenu(selfBtn)
  end)
  ui.fontBtn.tip = "Choose note font"
  bindTip(ui.fontBtn)

  ui.fontSizeBtn = btn(appearanceRow, 20, ICON.fontSize, "Text size")
  ui.fontSizeBtn:SetPoint("LEFT", ui.fontBtn, "RIGHT", 4, 0)
  ui.fontSizeBtn:SetScript("OnClick", function(selfBtn)
    if selfBtn.enabledState == false then return end
    AN:ShowFontSizeMenu(selfBtn)
  end)

  ui.fontColorBtn = btn(appearanceRow, 20, ICON.ok, "Text color")
  ui.fontColorBtn:SetPoint("LEFT", ui.fontSizeBtn, "RIGHT", 4, 0)
  if ui.fontColorBtn.icon then
    ui.fontColorBtn.icon:SetTexture(nil)
  end
  ui.fontColorBtn.swatch = ui.fontColorBtn:CreateTexture(nil, "ARTWORK")
  ui.fontColorBtn.swatch:SetPoint("TOPLEFT", 4, -4)
  ui.fontColorBtn.swatch:SetPoint("BOTTOMRIGHT", -4, 4)
  ui.fontColorBtn.swatch:SetColorTexture(0.95, 0.95, 0.95, 1)
  ui.fontColorBtn:SetScript("OnClick", function(selfBtn)
    if selfBtn.enabledState == false then return end
    AN:CloseAppearanceMenus()
    local selected = AN.UI.selectedID and AN:GetNoteByID(AN.UI.selectedID)
    if not selected or selected.isShared then return end
    local appearance = AN:GetNoteAppearance(selected)
    local initialColor = appearance.fontColor or { r = 0.95, g = 0.95, b = 0.95 }
    OpenNativeColorPicker(initialColor, function(r, g, b)
      AN:SetSelectedNoteFontColor(r, g, b)
    end, function(r, g, b)
      AN:SetSelectedNoteFontColor(r, g, b)
    end)
  end)

  ui.appearanceDivider = textShell:CreateTexture(nil, "BORDER")
  ui.appearanceDivider:SetPoint("TOPLEFT", appearanceRow, "BOTTOMLEFT", 0, -3)
  ui.appearanceDivider:SetPoint("TOPRIGHT", appearanceRow, "BOTTOMRIGHT", 0, -3)
  ui.appearanceDivider:SetHeight(1)
  ui.appearanceDivider:SetColorTexture(0, 0, 0, 1)

  local usingAceMulti = false
  local multiEdit
  local scroll
  local edit
  if AceGUI and AceGUI.Create then
    multiEdit = AceGUI:Create("MultiLineEditBox")
  end

  if multiEdit then
    usingAceMulti = true
    multiEdit:SetLabel("")
    multiEdit:DisableButton(true)
    multiEdit:SetMaxLetters(0)
    multiEdit.frame:SetParent(inner)
    multiEdit.frame:ClearAllPoints()
    multiEdit.frame:SetPoint("TOPLEFT", textShell, "TOPLEFT", 0, 0)
    multiEdit.frame:SetPoint("BOTTOMRIGHT", textShell, "BOTTOMRIGHT", 0, 0)
    multiEdit.frame:Show()
    if multiEdit.label then
      multiEdit.label:Hide()
    end
    if multiEdit.scrollBG then
      multiEdit.scrollBG:Hide()
      if multiEdit.scrollBG.SetBackdrop then
        multiEdit.scrollBG:SetBackdrop(nil)
      end
    end

    scroll = multiEdit.scrollFrame
    edit = multiEdit.editBox

    scroll:ClearAllPoints()
    scroll:SetPoint("TOPLEFT", textShell, "TOPLEFT", 4, -34)
    scroll:SetPoint("BOTTOMRIGHT", textShell, "BOTTOMRIGHT", -24, 4)

    local sb = multiEdit.scrollBar or scroll.ScrollBar or _G[scroll:GetName() .. "ScrollBar"]
    if sb then
      sb:ClearAllPoints()
      sb:SetPoint("TOPLEFT", textShell, "TOPRIGHT", 2, -16)
      sb:SetPoint("BOTTOMLEFT", textShell, "BOTTOMRIGHT", 2, 16)
      scroll.ScrollBar = sb
    end

    -- AceGUI's MultiLineEditBox uses an OnMouseUp handler on the scrollframe that
    -- moves the caret to end-of-text. For a notepad this breaks drag-selection and
    -- causes jump-to-bottom behavior, so disable those drag/drop-oriented scripts.
    scroll:SetScript("OnMouseUp", nil)
    scroll:SetScript("OnReceiveDrag", nil)
    edit:SetScript("OnMouseDown", nil)
    edit:SetScript("OnReceiveDrag", nil)

    edit:ClearAllPoints()
    edit:SetAllPoints(scroll)
    edit:SetAutoFocus(false)
    edit:SetFontObject("ChatFontNormal")
    edit:SetTextColor(0.95, 0.95, 0.95, 1)
    edit:SetJustifyH("LEFT")
    edit:SetJustifyV("TOP")
    edit:SetMaxLetters(0)
    if edit.SetAltArrowKeyMode then
      -- Use plain Up/Down arrows for caret navigation like a text editor.
      edit:SetAltArrowKeyMode(false)
    end
    if edit.SetCountInvisibleLetters then
      edit:SetCountInvisibleLetters(true)
    end
    edit:EnableMouse(true)
  else
    scroll = CreateFrame("ScrollFrame", "AmmeNotepad_ScrollFrame", inner, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", textShell, "TOPLEFT", 4, -34)
    scroll:SetPoint("BOTTOMRIGHT", textShell, "BOTTOMRIGHT", -24, 4)
    if scroll.ScrollBar then
      scroll.ScrollBar:ClearAllPoints()
      scroll.ScrollBar:SetPoint("TOPLEFT", textShell, "TOPRIGHT", 2, -16)
      scroll.ScrollBar:SetPoint("BOTTOMLEFT", textShell, "BOTTOMRIGHT", 2, 16)
    end
    edit = CreateFrame("EditBox", "AmmeNotepad_EditBox", scroll)
    edit:SetMultiLine(true)
    edit:SetAutoFocus(false)
    edit:SetFontObject("ChatFontNormal")
    edit:SetMaxLetters(0)
    if edit.SetAltArrowKeyMode then
      edit:SetAltArrowKeyMode(false)
    end
    edit:SetTextColor(0.95, 0.95, 0.95, 1)
    edit:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0)
    edit:SetWidth(math.max(100, scroll:GetWidth()))
    edit:SetHeight(math.max(100, scroll:GetHeight()))
    edit:SetJustifyH("LEFT")
    edit:SetJustifyV("TOP")
    if edit.SetCountInvisibleLetters then
      edit:SetCountInvisibleLetters(true)
    end
    edit:EnableMouse(true)
    edit.scrollFrame = scroll
    if type(_G.ScrollingEdit_OnCursorChanged) == "function" then
      edit:SetScript("OnCursorChanged", _G.ScrollingEdit_OnCursorChanged)
    end
    if type(_G.InputScrollFrame_OnUpdate) == "function" then
      edit:SetScript("OnUpdate", _G.InputScrollFrame_OnUpdate)
    elseif type(_G.ScrollingEdit_OnUpdate) == "function" then
      edit:SetScript("OnUpdate", _G.ScrollingEdit_OnUpdate)
    end
  end
  local caret = edit:CreateTexture(nil, "OVERLAY")
  if usingAceMulti then
    caret:Hide()
  else
    caret:SetColorTexture(0.95, 0.95, 0.95, 0.95)
    caret:SetSize(1, 14)
    caret:Hide()
  end
  local function updateCaretPos(x, y, h)
    if usingAceMulti then return end
    x = tonumber(x) or 0
    y = tonumber(y) or 0
    h = tonumber(h) or 14
    caret:ClearAllPoints()
    caret:SetPoint("TOPLEFT", edit, "TOPLEFT", x, y)
    caret:SetHeight(math.max(8, h))
  end
  local function stopCaretBlink()
    if usingAceMulti then return end
    if ui.caretTicker then
      ui.caretTicker:Cancel()
      ui.caretTicker = nil
    end
    caret:Hide()
  end
  local function startCaretBlink()
    if usingAceMulti then return end
    stopCaretBlink()
    if not edit:HasFocus() then return end
    caret:Show()
    ui.caretTicker = C_Timer.NewTicker(0.53, function()
      if not edit:HasFocus() or not f:IsShown() then
        stopCaretBlink()
        return
      end
      if caret:IsShown() then caret:Hide() else caret:Show() end
    end)
  end
  local function suppressCaretFollow()
    ui.followCursor = false
    ui.manualScrollUntil = (GetTime and GetTime() or 0) + 999
    if not usingAceMulti then
      stopCaretBlink()
    end
  end

  -- Do not attach mouse down/up handlers to the editbox; keep drag-selection
  -- behavior entirely native.
  edit:HookScript("OnMouseDown", function(selfEdit)
    ui._caretClickTracked = true
    ui._caretClickStartFocus = selfEdit:HasFocus()
    ui._caretClickStartCursor = tonumber(selfEdit:GetCursorPosition()) or 0
    if (usingAceMulti or ui.readOnlyInputLocked) and not selfEdit:HasFocus() then
      selfEdit:SetFocus()
    end
    if not usingAceMulti then
      stopCaretBlink()
    end
  end)
  edit:HookScript("OnMouseUp", function(selfEdit)
    ui.followCursor = true
    ui.manualScrollUntil = 0
    if (not usingAceMulti) and (not ui.readOnlyInputLocked) then
      startCaretBlink()
    end
    if ui._caretClickTracked then
      local hadFocus = (ui._caretClickStartFocus == true)
      local hadCursor = tonumber(ui._caretClickStartCursor) or 0
      local hasFocus = selfEdit and selfEdit.HasFocus and selfEdit:HasFocus() or false
      local hasCursor = tonumber((selfEdit and selfEdit.GetCursorPosition and selfEdit:GetCursorPosition()) or 0) or 0
      local focusGained = (not hadFocus) and hasFocus
      local caretMoved = (hasCursor ~= hadCursor)
      local readOnlyClick = (ui.readOnlyInputLocked == true)
      if (focusGained or caretMoved or readOnlyClick) and AN then
        if AN.CloseAppearanceMenus then
          AN:CloseAppearanceMenus()
        end
        if AN.CloseShareMenu then
          AN:CloseShareMenu()
        end
        if AN.CloseFolderMenu then
          AN:CloseFolderMenu()
        end
      end
    end
    ui._caretClickTracked = nil
    ui._caretClickStartFocus = nil
    ui._caretClickStartCursor = nil
  end)
  edit:HookScript("OnEditFocusGained", function(selfEdit)
    ui.followCursor = true
    ui.manualScrollUntil = 0
    if (not usingAceMulti) and (not ui.readOnlyInputLocked) then
      startCaretBlink()
    end
  end)
  edit:HookScript("OnEditFocusLost", function()
    if not usingAceMulti then
      stopCaretBlink()
    end
  end)
  edit:HookScript("OnKeyDown", function(_, key)
    if usingAceMulti then
      return
    end
    if key == "UP" or key == "DOWN" or key == "LEFT" or key == "RIGHT" or key == "HOME" or key == "END" or key == "PAGEUP" or key == "PAGEDOWN" then
      ui.followCursor = true
      ui.manualScrollUntil = 0
      startCaretBlink()
    end
    if key == "UP" then
      local startPos = tonumber(edit:GetCursorPosition()) or 0
      MoveCaretVertical(edit, ui, -1, startPos)
    elseif key == "DOWN" then
      local startPos = tonumber(edit:GetCursorPosition()) or 0
      MoveCaretVertical(edit, ui, 1, startPos)
    end
  end)
  local syncScrollBar
  local function updateEditHitRect()
    if usingAceMulti then return end
    if not edit.SetHitRectInsets then return end
    local offset = scroll:GetVerticalScroll() or 0
    local editH = edit:GetHeight() or 0
    local viewH = scroll:GetHeight() or 0
    if editH <= 0 or viewH <= 0 then
      edit:SetHitRectInsets(0, 0, 0, 0)
      return
    end
    local topInset = clamp(offset, 0, editH)
    local bottomInset = clamp(editH - offset - viewH, 0, editH)
    edit:SetHitRectInsets(0, 0, topInset, bottomInset)
  end
  local function updateEditMetrics()
    if usingAceMulti then
      syncScrollBar()
      return
    end
    local textH = edit.GetTextHeight and edit:GetTextHeight() or 0
    local contentH = math.max(scroll:GetHeight(), textH + 24)
    edit:SetWidth(math.max(100, scroll:GetWidth()))
    edit:SetHeight(contentH)
    scroll:UpdateScrollChildRect()
    updateEditHitRect()
    syncScrollBar()
  end
  if usingAceMulti then
    scroll:HookScript("OnSizeChanged", updateEditMetrics)
  else
    scroll:SetScript("OnSizeChanged", updateEditMetrics)
  end
  scroll:HookScript("OnMouseDown", function()
    if usingAceMulti and edit and edit.SetFocus and not edit:HasFocus() then
      edit:SetFocus()
    end
  end)
  scroll:HookScript("OnMouseUp", function()
    if usingAceMulti and edit and edit.SetFocus and not edit:HasFocus() then
      edit:SetFocus()
    end
  end)
  syncScrollBar = function()
    local sb = scroll.ScrollBar
    if not sb then return end
    local range = scroll:GetVerticalScrollRange() or 0
    sb:SetMinMaxValues(0, range)
    sb:SetValue(scroll:GetVerticalScroll())
    -- Keep scrollbar visible; disable/fade it when there is no scroll range.
    local shouldEnable = range > 1
    if ui.resizing then
      if not sb:IsShown() then sb:Show() end
      return
    end
    if not sb:IsShown() then sb:Show() end
    if sb.SetEnabled then
      sb:SetEnabled(shouldEnable)
    end
    if shouldEnable then
      sb:SetAlpha(1)
    else
      sb:SetAlpha(0.45)
    end
  end
  scroll:HookScript("OnVerticalScroll", function(selfScroll, offset)
    if (not usingAceMulti) and (not ui._settingScroll) then
      suppressCaretFollow()
    end
    if not usingAceMulti then
      updateEditHitRect()
    end
    syncScrollBar()
  end)
  scroll:HookScript("OnScrollRangeChanged", function()
    if not usingAceMulti then
      updateEditHitRect()
    end
    syncScrollBar()
  end)
  scroll:EnableMouseWheel(true)
  scroll:SetScript("OnMouseWheel", function(selfScroll, delta)
    if not usingAceMulti then
      suppressCaretFollow()
    end
    local step = 36
    local newOffset = (selfScroll:GetVerticalScroll() or 0) - (delta * step)
    local range = selfScroll:GetVerticalScrollRange() or 0
    if newOffset < 0 then newOffset = 0 end
    if newOffset > range then newOffset = range end
    selfScroll:SetVerticalScroll(newOffset)
    if not usingAceMulti then
      updateEditHitRect()
    end
    syncScrollBar()
  end)
  edit:HookScript("OnEscapePressed", function()
    if IsAnyModalShown(ui) then return end
    AN:RequestHideMainWindow()
  end)
  edit:HookScript("OnCursorChanged", function(selfEdit, x, y, _, lineH)
    local mouseSelecting = (IsMouseButtonDown and IsMouseButtonDown("LeftButton")) and selfEdit:HasFocus() or false

    if not usingAceMulti then
      local editH = selfEdit:GetHeight(); if editH < 1 then editH = 1 end; local r = editH - scroll:GetHeight()
      local now = (GetTime and GetTime() or 0)
      local canFollowCursor = selfEdit:HasFocus() and (ui.followCursor ~= false) and (not mouseSelecting) and (now >= (ui.manualScrollUntil or 0))
      if r > 0 and canFollowCursor then
        local lh = tonumber(lineH) or 14
        local curY = clamp(-y, 0, r)
        local viewTop = scroll:GetVerticalScroll() or 0
        local viewH = scroll:GetHeight() or 0
        local viewBottom = viewTop + math.max(1, viewH - lh)
        local target = nil

        -- Only adjust when caret leaves the visible viewport to avoid jumpy recentering.
        if curY < viewTop then
          target = curY
        elseif curY > viewBottom then
          target = curY - math.max(1, viewH - lh)
        end

        if target then
          ui._settingScroll = true
          scroll:SetVerticalScroll(clamp(target, 0, r))
          ui._settingScroll = false
          syncScrollBar()
        end
      end
    end

    updateCaretPos(x, y, lineH)
    if AN.Debug and mouseSelecting then
      local nowDbg = GetTime and GetTime() or 0
      if not ui._dbgLastCursor or (nowDbg - ui._dbgLastCursor) > 0.15 then
        ui._dbgLastCursor = nowDbg
        local down = IsMouseButtonDown and IsMouseButtonDown("LeftButton")
        dbg(
          "cursor drag: x=%.1f y=%.1f h=%.1f pos=%d scroll=%.1f focus=%s lmb=%s",
          tonumber(x) or 0,
          tonumber(y) or 0,
          tonumber(lineH) or 0,
          tonumber(selfEdit:GetCursorPosition()) or -1,
          tonumber(scroll:GetVerticalScroll()) or 0,
          tostring(selfEdit:HasFocus()),
          tostring(down)
        )
      end
    end
  end)
  edit:HookScript("OnTextChanged", function(selfEdit, user)
    updateEditMetrics(); if not user or ui.block or not ui.selectedID then return end
    ui.lastText = ui.lastText or {}
    local activeNote = AN:GetNoteByID(ui.selectedID)
    if activeNote and activeNote.isShared then
      local prevShared = ui.lastText[ui.selectedID] or tostring(activeNote.body or "")
      local nowShared = selfEdit:GetText() or ""
      if nowShared ~= prevShared then
        local cur = tonumber(selfEdit:GetCursorPosition()) or 0
        ui.block = true
        selfEdit:SetText(prevShared)
        ui.block = false
        selfEdit:SetCursorPosition(clamp(cur - 1, 0, #prevShared))
        local tNow = GetTime and GetTime() or 0
        if not ui.readOnlyWarnAt or (tNow - ui.readOnlyWarnAt) > 1.5 then
          ui.readOnlyWarnAt = tNow
          AN:ShowFeedbackMessage("This shared tab is read-only.", { hold = 2.2 })
        end
      end
      return
    end
    local prev = ui.lastText[ui.selectedID] or ""; local now = selfEdit:GetText() or ""
    if now ~= prev then
      local h = hist(ui, ui.selectedID); if h.undo[#h.undo] ~= prev then h.undo[#h.undo+1] = prev; if #h.undo > 50 then table.remove(h.undo,1) end end; h.redo = {}
      if activeNote and (not activeNote.isShared) and activeNote.userSaved == true and activeNote.dirty ~= true then
        activeNote.dirty = true
        if AN.RefreshTabs then
          AN:RefreshTabs()
        end
      end
      ui.lastText[ui.selectedID] = now; AN:SaveCurrentNote(true); AN:RefreshURLState()
    end
  end)
  scroll:SetScrollChild(edit)
  updateEditMetrics()

  local grip = btn(textShell,16,ICON.resize,"Drag to resize notepad height"); grip:SetPoint("BOTTOMRIGHT", -2, 2)
  if grip.icon then grip.icon:SetAlpha(0.6) end
  if grip.bg then grip.bg:Hide() end
  if grip.t then grip.t:Hide() end
  if grip.b then grip.b:Hide() end
  if grip.l then grip.l:Hide() end
  if grip.r then grip.r:Hide() end
  grip:SetScript("OnMouseDown", function()
    ui.resizing = true
    local s = UIParent:GetEffectiveScale()
    local _, y = GetCursorPosition()
    ui.startY = y / s
    ui.startF = f:GetHeight()
    ui.startLeft = f:GetLeft()
    ui.startTop = f:GetTop()
    grip:SetScript("OnUpdate", function()
      if not ui.resizing then return end
      local _,cy = GetCursorPosition(); cy = cy/s
      local d = ui.startY - cy
      local newH = clamp(ui.startF + d, 620, 1100)
      -- Keep top edge fixed so resize expands/contracts only at the bottom.
      if ui.startLeft and ui.startTop then
        f:ClearAllPoints()
        f:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", ui.startLeft, ui.startTop)
      end
      f:SetHeight(newH)
    end)
  end)
  grip:SetScript("OnMouseUp", function()
    ui.resizing = false; grip:SetScript("OnUpdate", nil)
    local w = AN.DB.settings.window; w.editorHeight = ui.editorOuter:GetHeight(); w.height = f:GetHeight(); w.width = f:GetWidth()
    local p, _, rp, x, y = f:GetPoint(1)
    w.point = p or "CENTER"
    w.relPoint = rp or w.point
    w.x = x or 0
    w.y = y or 0
  end)

  ui.main = f; ui.edit = edit; ui.scroll = scroll; ui.multiEdit = multiEdit; ui.caret = caret; ui.updateEditMetrics = updateEditMetrics
  AN:ApplyUIScale(AN.DB and AN.DB.settings and AN.DB.settings.uiScale)

  local function updateSearchUX()
    local txt = ui.search:GetText() or ""
    local hasText = txt ~= ""
    ui.searchClear:SetShown(hasText)
    ui.searchPlaceholder:SetShown(not hasText)
  end

  ui.search:SetScript("OnEditFocusGained", function() ui.modeRow:Show(); updateSearchUX() end)
  ui.search:SetScript("OnEditFocusLost", function() C_Timer.After(0.1, function() if not ui.search:HasFocus() then ui.modeRow:Hide() end end) end)
  ui.search:SetScript("OnEscapePressed", function(selfEdit)
    selfEdit:ClearFocus()
  end)
  ui.search:SetScript("OnTextChanged", function(selfEdit)
    local c = sanitizeSearch(selfEdit:GetText())
    if c ~= selfEdit:GetText() then selfEdit:SetText(c); selfEdit:SetCursorPosition(#c) end
    updateSearchUX()
    AN:BuildSearchResults(c); AN:RefreshSearchDropdown(); AN:ApplySearchHighlightToCurrentNote()
  end)

  f:SetScript("OnShow", function()
    if ui.folderPanel then
      ui.folderPanel:Show()
      AN:SetFolderPanelExpanded(AN.DB.settings.folderPanelExpanded == true, false)
    end
    AN:RefreshFolderPanel()
  end)

  f:SetScript("OnHide", function()
    if ui.caretTicker then
      ui.caretTicker:Cancel()
      ui.caretTicker = nil
    end
    if ui.folderStatsPulseTicker then
      ui.folderStatsPulseTicker:Cancel()
      ui.folderStatsPulseTicker = nil
    end
    if ui.folderStatsLine2 then
      ui.folderStatsLine2:SetTextColor(0.90, 0.90, 0.90, 0.9)
    end
    AN:SaveCurrentNote(true)
    if ui.searchDrop then ui.searchDrop:Hide() end
    if AN.HideOpenContextMenus then
      AN:HideOpenContextMenus()
    else
      if ui.tabMenu then ui.tabMenu:Hide() end
      if ui.folderMenu then ui.folderMenu:Hide() end
      if ui.shareMenu then ui.shareMenu:Hide() end
      if ui.urlMenu then ui.urlMenu:Hide() end
    end
    if ui.folderSlideTicker then
      ui.folderSlideTicker:Cancel()
      ui.folderSlideTicker = nil
    end
    if ui.folderPanel then
      ui.folderPanel:Hide()
    end
  end)

  local mode = AN.DB.settings.searchMode or "name"; ui.modeName.dot:SetShown(mode=="name"); ui.modeContent.dot:SetShown(mode=="content")
  updateSearchUX()
  AN:SetFolderPanelExpanded(AN.DB.settings.folderPanelExpanded == true, false)
  AN:RefreshFolderPanel()
  local ids = AN:GetVisibleNoteIDs(); if #ids > 0 then AN:SelectNote(ids[1]) end

  if ui.urlTicker then ui.urlTicker:Cancel() end
  ui.urlTicker = C_Timer.NewTicker(3, function() if ui.main and ui.main:IsShown() then AN:RefreshURLState() end end)

  AN:RestartAutosaveTicker(); AN:RefreshAllUI(); AN:RefreshNoteActionStates(); f:Hide()
  if ui.folderPanel then ui.folderPanel:Hide() end
  AN:SyncMenuDismissLayer()

  if AN._PendingSharedImport then
    local pending = AN._PendingSharedImport
    AN._PendingSharedImport = nil
    if C_Timer and C_Timer.After then
      C_Timer.After(0, function()
        if AN and AN.ShowSharedImportPrompt then
          AN:ShowSharedImportPrompt(pending)
        end
      end)
    else
      AN:ShowSharedImportPrompt(pending)
    end
  end

  if AN._PendingRemoteImport then
    local pending = AN._PendingRemoteImport
    AN._PendingRemoteImport = nil
    if C_Timer and C_Timer.After then
      C_Timer.After(0, function()
        if AN and AN.ShowRemoteImportPrompt then
          AN:ShowRemoteImportPrompt(pending.from, pending.title, pending.shareID)
        end
      end)
    else
      AN:ShowRemoteImportPrompt(pending.from, pending.title, pending.shareID)
    end
  end
end
