diff --git a/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs b/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs
new file mode 100644
index 00000000..2e03db1f
--- /dev/null
+++ b/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs
@@ -0,0 +1,138 @@
+using DatReaderWriter;
+using DatReaderWriter.DBObjs;
+using DatReaderWriter.Types;
+
+namespace AcDream.App.UI.Layout;
+
+///
+/// Resolves the empty-slot background sprite for a UIElement_ItemList's cells the way retail
+/// does. Retail ref: UIElement_ItemList::InternalCreateItem (acclient_2013_pseudo_c.txt:231486 /
+/// 0x004e3570): GetAttribute_Enum(this, 0x1000000e, &id) -> GetByEnum(0x10000038,5,0x23) [the
+/// shared UIItem catalog LayoutDesc, = 0x21000037] -> CreateChildElement(catalog, id); the cloned
+/// prototype's ItemSlot_Empty (state 0x1000001c) media is the empty-cell background.
+///
+public static class ItemListCellTemplate
+{
+ /// The shared UIItem cell-template catalog. Hardcoded: retail resolves it via
+ /// GetByEnum(0x10000038,5,0x23) through a master enum-map DAT object (no code literal);
+ /// 0x21000037 is the only LayoutDesc that is a catalog of standalone UIItem (0x10000032)
+ /// prototypes. Validated by the real-dat test (the resolved sprite must be a real 0x06 surface).
+ public const uint CatalogLayoutId = 0x21000037u;
+
+ private const uint CellTemplateAttr = 0x1000000eu; // UIElement attribute: cell-template element id
+ private const uint IconChildId = 0x1000033Bu; // m_elem_Icon sub-element (carries ItemSlot_Empty)
+ private const string ItemSlotEmpty = "ItemSlot_Empty"; // UIStateId.ToString() for state 0x1000001c
+
+ ///
+ /// Resolve the empty-slot sprite (a 0x06xxxxxx RenderSurface id) for the cells of item-list
+ /// element in LayoutDesc .
+ /// Returns 0 when the layout/element/attribute/prototype/media is absent (the caller keeps
+ /// the default).
+ ///
+ public static uint ResolveEmptySprite(DatCollection dats, uint listLayoutId, uint listElementId)
+ {
+ var listLd = dats.Get(listLayoutId);
+ if (listLd is null) return 0;
+ var listElem = FindDesc(listLd, listElementId);
+ if (listElem is null) return 0;
+
+ uint protoId = ReadCellTemplateId(listElem); // attr 0x1000000e
+ if (protoId == 0) return 0;
+
+ var catalog = dats.Get(CatalogLayoutId);
+ if (catalog is null) return 0;
+ var proto = FindDesc(catalog, protoId);
+ if (proto is null) return 0;
+
+ // Priority: a child's DirectState background frame (the 36x36 backpack cell's recessed
+ // border, e.g. 0x06005D9C); else the m_elem_Icon child's ItemSlot_Empty media (the 32x32
+ // contents cell, e.g. 0x06004D20). See the spec's explicit single-sprite contract.
+ uint frame = FirstChildDirectStateImage(proto);
+ return frame != 0 ? frame : IconChildEmptyImage(proto);
+ }
+
+ // ── attribute 0x1000000e: stored as EnumBaseProperty in the dat (Value is the prototype id) ──
+ // Note: the spec anticipated DataIdBaseProperty/ArrayBaseProperty based on the font-DID pattern,
+ // but the live dat uses EnumBaseProperty.Value (uint) — confirmed by runtime reflection.
+ private static uint ReadCellTemplateId(ElementDesc elem)
+ {
+ uint id = ReadIdFromState(elem.StateDesc);
+ if (id != 0) return id;
+ foreach (var s in elem.States)
+ {
+ id = ReadIdFromState(s.Value);
+ if (id != 0) return id;
+ }
+ return 0;
+ }
+
+ private static uint ReadIdFromState(StateDesc? sd)
+ {
+ if (sd?.Properties is null) return 0;
+ return sd.Properties.TryGetValue(CellTemplateAttr, out var raw) ? ReadId(raw) : 0;
+ }
+
+ private static uint ReadId(BaseProperty raw)
+ {
+ if (raw is ArrayBaseProperty arr && arr.Value.Count > 0) return ReadId(arr.Value[0]);
+ if (raw is DataIdBaseProperty did) return did.Value;
+ if (raw is EnumBaseProperty ep) return ep.Value; // 0x1000000e is EnumBaseProperty in the live dat
+ return 0;
+ }
+
+ // ── prototype media ──
+ private static uint FirstChildDirectStateImage(ElementDesc proto)
+ {
+ foreach (var kv in proto.Children)
+ {
+ uint f = DirectStateImage(kv.Value);
+ if (f != 0) return f;
+ }
+ return 0;
+ }
+
+ private static uint IconChildEmptyImage(ElementDesc proto)
+ {
+ var icon = FindDescIn(proto, IconChildId); // recursive: handles inner wrappers (e.g. 0x10000340)
+ if (icon is null) return 0;
+ foreach (var s in icon.States)
+ if (s.Key.ToString() == ItemSlotEmpty)
+ {
+ uint f = StateImage(s.Value);
+ if (f != 0) return f;
+ }
+ return DirectStateImage(icon); // some prototypes carry empty media on the DirectState
+ }
+
+ private static uint DirectStateImage(ElementDesc d)
+ => d.StateDesc is null ? 0u : StateImage(d.StateDesc);
+
+ private static uint StateImage(StateDesc sd)
+ {
+ foreach (var m in sd.Media)
+ if (m is MediaDescImage img && img.File != 0) return img.File;
+ return 0;
+ }
+
+ // ── depth-first element search by id (LayoutImporter.FindDesc is private there) ──
+ private static ElementDesc? FindDesc(LayoutDesc ld, uint id)
+ {
+ foreach (var kv in ld.Elements)
+ {
+ var f = FindDescIn(kv.Value, id);
+ if (f is not null) return f;
+ }
+ return null;
+ }
+
+ private static ElementDesc? FindDescIn(ElementDesc d, uint id)
+ {
+ if (d.ElementId == id) return d;
+ foreach (var kv in d.Children)
+ {
+ var f = FindDescIn(kv.Value, id);
+ if (f is not null) return f;
+ }
+ return null;
+ }
+}
diff --git a/tests/AcDream.App.Tests/UI/Layout/ItemListCellTemplateTests.cs b/tests/AcDream.App.Tests/UI/Layout/ItemListCellTemplateTests.cs
new file mode 100644
index 00000000..898f9aac
--- /dev/null
+++ b/tests/AcDream.App.Tests/UI/Layout/ItemListCellTemplateTests.cs
@@ -0,0 +1,59 @@
+using System;
+using System.IO;
+using AcDream.App.UI.Layout;
+using DatReaderWriter;
+using DatReaderWriter.Options;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace AcDream.App.Tests.UI.Layout;
+
+///
+/// Real-dat test for : the inventory
+/// item-lists must resolve their empty-slot sprite from the dat cell template (attribute
+/// 0x1000000e -> catalog 0x21000037 prototype's ItemSlot_Empty), NOT the generic toolbar
+/// square 0x060074CF. Also PRINTS the resolved ids — the "pin the exact asset" record.
+/// Skips when the live dat directory is absent (CI).
+///
+public class ItemListCellTemplateTests
+{
+ private const uint Generic = 0x060074CFu; // generic toolbar-shortcut empty square (the bug)
+
+ private const uint ContentsLayout = 0x21000021u; private const uint ContentsGrid = 0x100001C6u;
+ private const uint BackpackLayout = 0x21000022u; private const uint SideBag = 0x100001CAu;
+ private const uint MainPack = 0x100001C9u;
+
+ private readonly ITestOutputHelper _out;
+ public ItemListCellTemplateTests(ITestOutputHelper o) => _out = o;
+
+ private static string? DatDir()
+ {
+ var d = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
+ ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ "Documents", "Asheron's Call");
+ return Directory.Exists(d) ? d : null;
+ }
+
+ [Fact]
+ public void Inventory_lists_resolve_a_real_nongeneric_empty_sprite()
+ {
+ var datDir = DatDir();
+ if (datDir is null) return; // CI: no live dat — skip (smoke test)
+
+ using var dats = new DatCollection(datDir, DatAccessType.Read);
+
+ foreach (var (name, layout, elem) in new[]
+ {
+ ("contents", ContentsLayout, ContentsGrid),
+ ("side-bag", BackpackLayout, SideBag),
+ ("main-pack", BackpackLayout, MainPack),
+ })
+ {
+ uint sprite = ItemListCellTemplate.ResolveEmptySprite(dats, layout, elem);
+ _out.WriteLine($"{name}: list 0x{elem:X8} (layout 0x{layout:X8}) -> empty sprite 0x{sprite:X8}");
+ Assert.True(sprite != 0u, $"{name}: resolved 0 (no 0x1000000e attr / prototype / media)");
+ Assert.True(sprite != Generic, $"{name}: resolved the generic 0x060074CF — the bug, not the fix");
+ Assert.True((sprite & 0xFF000000u) == 0x06000000u, $"{name}: 0x{sprite:X8} is not a 0x06 RenderSurface id");
+ }
+ }
+}