diff --git a/src/AcDream.App/UI/UiNineSlicePanel.cs b/src/AcDream.App/UI/UiNineSlicePanel.cs
new file mode 100644
index 00000000..2f04229a
--- /dev/null
+++ b/src/AcDream.App/UI/UiNineSlicePanel.cs
@@ -0,0 +1,85 @@
+using System.Numerics;
+
+namespace AcDream.App.UI;
+
+///
+/// A whose background is the retail 8-piece window bevel
+/// (): 4 corners + 4 edges around a tiled
+/// center fill. Retires the flat translucent rect (divergence row TS-30).
+/// Sprites resolve to (GL handle, width, height) via an injected delegate so
+/// the widget is testable without GL. In production:
+/// id => { var t = cache.GetOrUploadRenderSurface(id, out var w, out var h); return (t, w, h); }.
+///
+public sealed class UiNineSlicePanel : UiPanel
+{
+ /// A placed chrome piece: destination rect in local pixel space.
+ public readonly record struct Rect(float X, float Y, float W, float H);
+
+ /// The nine destination rects for an 8-piece border + center.
+ public readonly record struct FrameRects(
+ Rect Center, Rect Top, Rect Bottom, Rect Left, Rect Right,
+ Rect TL, Rect TR, Rect BL, Rect BR);
+
+ private readonly System.Func _resolve;
+
+ public UiNineSlicePanel(System.Func resolve)
+ {
+ _resolve = resolve;
+ BackgroundColor = Vector4.Zero; // suppress the base flat-rect fill
+ BorderColor = Vector4.Zero;
+ }
+
+ ///
+ /// Destination rects (local px) for a frame of (,
+ /// ) with border thickness :
+ /// b×b corners, top/bottom edges spanning the interior width at height b,
+ /// left/right edges spanning the interior height at width b, center fills
+ /// the interior.
+ ///
+ public static FrameRects ComputeFrameRects(float w, float h, int b)
+ {
+ float innerW = w - 2 * b;
+ float innerH = h - 2 * b;
+ return new FrameRects(
+ Center: new Rect(b, b, innerW, innerH),
+ Top: new Rect(b, 0, innerW, b),
+ Bottom: new Rect(b, h - b, innerW, b),
+ Left: new Rect(0, b, b, innerH),
+ Right: new Rect(w - b, b, b, innerH),
+ TL: new Rect(0, 0, b, b),
+ TR: new Rect(w - b, 0, b, b),
+ BL: new Rect(0, h - b, b, b),
+ BR: new Rect(w - b, h - b, b, b));
+ }
+
+ protected override void OnDraw(UiRenderContext ctx)
+ {
+ var r = ComputeFrameRects(Width, Height, RetailChromeSprites.Border);
+ // center + edges tile (UV repeat); corners stretch 1:1.
+ DrawTiled(ctx, RetailChromeSprites.CenterFill, r.Center);
+ DrawTiled(ctx, RetailChromeSprites.TopEdge, r.Top);
+ DrawTiled(ctx, RetailChromeSprites.BottomEdge, r.Bottom);
+ DrawTiled(ctx, RetailChromeSprites.LeftEdge, r.Left);
+ DrawTiled(ctx, RetailChromeSprites.RightEdge, r.Right);
+ DrawStretched(ctx, RetailChromeSprites.CornerTL, r.TL);
+ DrawStretched(ctx, RetailChromeSprites.CornerTR, r.TR);
+ DrawStretched(ctx, RetailChromeSprites.CornerBL, r.BL);
+ DrawStretched(ctx, RetailChromeSprites.CornerBR, r.BR);
+ }
+
+ private void DrawTiled(UiRenderContext ctx, uint id, Rect d)
+ {
+ if (d.W <= 0 || d.H <= 0) return;
+ var (tex, tw, th) = _resolve(id);
+ if (tex == 0 || tw == 0 || th == 0) return;
+ ctx.DrawSprite(tex, d.X, d.Y, d.W, d.H, 0, 0, d.W / tw, d.H / th, Vector4.One);
+ }
+
+ private void DrawStretched(UiRenderContext ctx, uint id, Rect d)
+ {
+ if (d.W <= 0 || d.H <= 0) return;
+ var (tex, _, _) = _resolve(id);
+ if (tex == 0) return;
+ ctx.DrawSprite(tex, d.X, d.Y, d.W, d.H, 0, 0, 1, 1, Vector4.One);
+ }
+}
diff --git a/tests/AcDream.App.Tests/UI/UiNineSlicePanelTests.cs b/tests/AcDream.App.Tests/UI/UiNineSlicePanelTests.cs
new file mode 100644
index 00000000..8a2b3d0a
--- /dev/null
+++ b/tests/AcDream.App.Tests/UI/UiNineSlicePanelTests.cs
@@ -0,0 +1,27 @@
+using AcDream.App.UI;
+
+namespace AcDream.App.Tests.UI;
+
+public class UiNineSlicePanelTests
+{
+ [Fact]
+ public void ComputeFrameRects_PlacesCornersEdgesAndCenter()
+ {
+ var r = UiNineSlicePanel.ComputeFrameRects(100, 80, 5);
+
+ // 5x5 corners at the four corners
+ Assert.Equal(new UiNineSlicePanel.Rect(0, 0, 5, 5), r.TL);
+ Assert.Equal(new UiNineSlicePanel.Rect(95, 0, 5, 5), r.TR);
+ Assert.Equal(new UiNineSlicePanel.Rect(0, 75, 5, 5), r.BL);
+ Assert.Equal(new UiNineSlicePanel.Rect(95, 75, 5, 5), r.BR);
+
+ // edges span the interior (100-2*5 = 90 wide, 80-2*5 = 70 tall)
+ Assert.Equal(new UiNineSlicePanel.Rect(5, 0, 90, 5), r.Top);
+ Assert.Equal(new UiNineSlicePanel.Rect(5, 75, 90, 5), r.Bottom);
+ Assert.Equal(new UiNineSlicePanel.Rect(0, 5, 5, 70), r.Left);
+ Assert.Equal(new UiNineSlicePanel.Rect(95, 5, 5, 70), r.Right);
+
+ // center fills the interior
+ Assert.Equal(new UiNineSlicePanel.Rect(5, 5, 90, 70), r.Center);
+ }
+}