diff --git a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
index ec053cee..11512b0b 100644
--- a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
+++ b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs
@@ -707,11 +707,17 @@ public static class DatWidgetFactory
// newlines (the fellowship empty-state is three sentences over
// '\n's). A single Line renders them as one clipped run — split
// into one Line per authored line, exactly as retail's multiline
- // UIElement_Text draws them. The provider re-reads DefaultColor
+ // UIElement_Text draws them. Gate round 2: the DAT stores the
+ // LITERAL two-character escape "\n" (0x5C 0x6E — probe-verified:
+ // the dump printed backslash-n, not a line break), so normalize
+ // the escape before splitting. The provider re-reads DefaultColor
// per call (NOT captured eagerly) so state-driven font-color
// changes keep tracking, the same live-color contract the
// single-line provider always had.
- string[] parts = [.. authored.Split('\n').Select(static p => p.TrimEnd('\r'))];
+ string[] parts = [.. authored
+ .Replace("\\n", "\n")
+ .Split('\n')
+ .Select(static p => p.TrimEnd('\r'))];
t.LinesProvider = () =>
[.. parts.Select(p => new UiText.Line(p, t.DefaultColor))];
}
diff --git a/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs b/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs
index 14fb4ea7..69f50dab 100644
--- a/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs
+++ b/src/AcDream.App/UI/Layout/SocialFellowshipPageController.cs
@@ -102,6 +102,13 @@ public sealed class SocialFellowshipPageController
private const uint ShareLootCheckboxId = 0x10000273u;
// ── Row-template element ids (same doc, the 8-part fellow row) ──
+ /// Gate round 2 (2026-08-13): the row's name BAND — probe-verified
+ /// to author the retail selected-row art: DirectState media
+ /// 0x06001450 + a 'Highlight' state with the amber
+ /// 0x06001451 (ProbeSocialClickRouting's ROWSTATE dump). Selection
+ /// flips its — the authored
+ /// mechanism, replacing the retired invented tints.
+ private const uint RowNameBandId = 0x10000282u;
private const uint RowNameTextId = 0x10000283u;
private const uint RowStatsTextId = 0x10000284u;
private const uint RowHealthMeterId = 0x10000285u;
@@ -168,6 +175,7 @@ public sealed class SocialFellowshipPageController
Func ResolveString);
private readonly record struct FellowRowWidgets(
+ UiDatElement? NameBand,
UiText? Name,
UiText? Stats,
UiMeter? Health,
@@ -716,6 +724,7 @@ public sealed class SocialFellowshipPageController
}
var widgets = new FellowRowWidgets(
+ UiElement.FindDescendant(row, RowNameBandId) as UiDatElement,
UiElement.FindDescendant(row, RowNameTextId) as UiText,
UiElement.FindDescendant(row, RowStatsTextId) as UiText,
UiElement.FindDescendant(row, RowHealthMeterId) as UiMeter,
@@ -757,6 +766,11 @@ public sealed class SocialFellowshipPageController
nameText.LinesProvider = () => [new UiText.Line(name, MemberNameColor)];
}
+ // Gate round 2: the AUTHORED selected-row cue — the name band's
+ // 'Highlight' state is retail's amber (see RowNameBandId's doc).
+ if (widgets.NameBand is { } nameBand)
+ nameBand.ActiveState = _selectedFellowGuid == member.Guid ? "Highlight" : "";
+
if (widgets.Stats is { } statsText)
{
string text = FormatStatsText(member, snapshot);
diff --git a/src/AcDream.App/UI/UiRoot.cs b/src/AcDream.App/UI/UiRoot.cs
index 61dac04e..84f7f9f1 100644
--- a/src/AcDream.App/UI/UiRoot.cs
+++ b/src/AcDream.App/UI/UiRoot.cs
@@ -148,9 +148,13 @@ public sealed class UiRoot : UiElement
var window = FindWindow(target);
if (window is not { Draggable: true })
return false;
- return !target.IsDragSource
- && !target.CapturesPointerDrag
- && !target.HandlesClick;
+ // 2026-08-13 gate (user-directed, all windows): the move cursor
+ // advertises ONLY on the window's own chrome — the frame's border
+ // pixels are the only place the frame element itself wins the
+ // hit-test (interior points resolve to content children), which
+ // matches retail's Dragbar-chrome-only move cursor. Whole-surface
+ // dragging still WORKS; it just no longer advertises over content.
+ return ReferenceEquals(target, window);
}
}
private (uint tex, int w, int h)? _dragGhost;
diff --git a/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs
index 620055a3..5e4eeb8f 100644
--- a/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs
+++ b/tests/AcDream.App.Tests/UI/Layout/SocialPanelLiveMountProbeTests.cs
@@ -637,6 +637,20 @@ public sealed class SocialPanelLiveMountProbeTests
$"[clickprobe] NOT-in-fellowship synthetic click -> optionWrites=[{string.Join(",", optionWrites)}]");
}
+ // The fellow row template's authored STATES (gate round 2: retail's
+ // selected-row amber — which element carries a Highlight/selected
+ // state, and what media?).
+ if (UiElement.FindDescendant(uiRoot, 0x10000279u) is UiTemplateListBox rosterBox
+ && rosterBox.Templates.Count > 0)
+ {
+ ElementInfo? rowInfo = LayoutImporter.ImportInfos(
+ dats,
+ rosterBox.Templates[0].TemplateLayoutId,
+ rosterBox.Templates[0].TemplateElementId);
+ if (rowInfo is not null)
+ DumpStates(rowInfo, 0);
+ }
+
// Friends + Squelch action widgets: authored labels → button roles
// (never guess an id's role).
foreach (uint pageId in new[] { 0x10000513u, 0x1000054Au })
@@ -672,6 +686,19 @@ public sealed class SocialPanelLiveMountProbeTests
return (x, y);
}
+ private static void DumpStates(ElementInfo info, int depth)
+ {
+ string states = string.Join(
+ ",",
+ info.StateMedia.Select(kv => $"'{kv.Key}'=0x{kv.Value.File:X8}"));
+ Console.WriteLine(
+ $"[clickprobe] {new string(' ', depth * 2)}ROWSTATE 0x{info.Id:X8} type=0x{info.Type:X8} "
+ + $"({info.X},{info.Y} {info.Width}x{info.Height}) default='{info.DefaultStateName}' "
+ + $"media=[{states}]");
+ foreach (ElementInfo c in info.Children)
+ DumpStates(c, depth + 1);
+ }
+
private static void DumpActionWidgets(UiElement el, int depth)
{
string extra = el switch