diff --git a/html/shop.css b/html/shop.css
new file mode 100644
index 0000000..f9da4d3
--- /dev/null
+++ b/html/shop.css
@@ -0,0 +1,165 @@
+/* html/shop.css */
+
+/* Hide helpers */
+#shop-wrap.hidden { display: none; }
+.hidden { display: none !important; }
+
+/* Kill any “black frame” / overlay behind the card */
+html, body {
+ background: transparent !important;
+}
+
+#shop-wrap {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: auto;
+ background: transparent !important;
+ z-index: 50;
+}
+
+/* If shop is open, hide ATM vignette even if wrap accidentally exists */
+#shop-wrap:not(.hidden) ~ #wrap::before { display: none !important; }
+#wrap::before { display: none !important; } /* temporary test */
+
+/* Card (gang-tinted glass) */
+.shop-card {
+ width: 520px;
+ border-radius: 16px;
+ padding: 18px;
+
+ /* Gang-tinted glass */
+ background: linear-gradient(
+ 180deg,
+ rgba(var(--gang-rgb, 10, 10, 10), 0.45),
+ rgba(10,10,10,0.60)
+ );
+
+ /* ✅ remove the frame */
+ border: none;
+ outline: none;
+ box-shadow: 0 10px 26px rgba(var(--gang-rgb, 10, 10, 10), 0.18);
+
+ color: #fff;
+ backdrop-filter: blur(12px);
+}
+
+
+/* Header */
+.shop-top {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
+.shop-title {
+ font-size: 20px;
+ font-weight: 800;
+ margin: 0;
+ line-height: 1.2;
+}
+
+.shop-sub {
+ font-size: 12px;
+ opacity: 0.85;
+ margin-top: 6px;
+}
+
+.shop-balance {
+ text-align: right;
+ font-size: 12px;
+ opacity: 0.9;
+}
+.shop-balance .num {
+ font-size: 18px;
+ font-weight: 800;
+ margin-top: 2px;
+}
+
+/* Tabs */
+.shop-tabs {
+ display: flex;
+ gap: 8px;
+ margin: 12px 0 10px;
+}
+
+.shop-tab {
+ border: 1px solid rgba(255,255,255,0.14);
+ background: rgba(255,255,255,0.06);
+ color: #fff;
+ padding: 8px 10px;
+ border-radius: 10px;
+ cursor: pointer;
+ font-size: 13px;
+ user-select: none;
+}
+
+.shop-tab.active {
+ background: rgba(var(--gang-rgb, 10, 10, 10), 0.28);
+ border-color: rgba(var(--gang-rgb, 10, 10, 10), 0.55);
+}
+
+/* List */
+.shop-list {
+ max-height: 360px;
+ overflow: auto;
+ border-top: 1px solid rgba(255,255,255,0.10);
+ padding-top: 10px;
+}
+
+.shop-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 10px 0;
+ border-bottom: 1px solid rgba(255,255,255,0.08);
+}
+
+.shop-item-name { font-weight: 700; }
+
+.shop-item-sub {
+ font-size: 12px;
+ opacity: 0.85;
+ margin-top: 2px;
+}
+
+.shop-price {
+ font-weight: 800;
+ margin-right: 10px;
+ white-space: nowrap;
+}
+
+/* Buttons */
+.shop-buy {
+ border: 1px solid rgba(255,255,255,0.14);
+ background: rgba(255,255,255,0.08);
+ color: #fff;
+ padding: 8px 12px;
+ border-radius: 10px;
+ cursor: pointer;
+ font-weight: 700;
+}
+.shop-buy:hover {
+ background: rgba(255,255,255,0.12);
+}
+
+/* Footer */
+.shop-footer {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 12px;
+ gap: 10px;
+ font-size: 12px;
+ opacity: 0.85;
+}
+
+.shop-close {
+ cursor: pointer;
+ text-decoration: underline;
+ opacity: 0.9;
+}
diff --git a/html/shop.js b/html/shop.js
new file mode 100644
index 0000000..e8ae769
--- /dev/null
+++ b/html/shop.js
@@ -0,0 +1,249 @@
+// html/shop.js
+(() => {
+ const wrap = document.getElementById("shop-wrap");
+ const title = document.getElementById("shop-title");
+ const balanceNum = document.getElementById("shop-balance-num");
+
+ const tabBtns = {
+ weapons: document.getElementById("shop-tab-weapons"),
+ ammo: document.getElementById("shop-tab-ammo"),
+ vehicles: document.getElementById("shop-tab-vehicles"),
+ };
+
+ const list = document.getElementById("shop-list");
+ const closeBtn = document.getElementById("shop-close");
+
+ console.log("[turfwar] shop.js loaded");
+
+ // Guard against missing markup (prevents silent failures)
+ if (
+ !wrap || !title || !balanceNum || !list || !closeBtn ||
+ !tabBtns.weapons || !tabBtns.ammo || !tabBtns.vehicles
+ ) {
+ console.error("[turfwar] shop.js missing DOM elements. Check atm.html includes #shop-wrap and tab ids.");
+ return;
+ }
+
+ const ALL_TABS = ["weapons", "ammo", "vehicles"];
+
+ let state = {
+ open: false,
+ tab: "weapons",
+ gangId: 0,
+ balance: 0,
+ payload: { weapons: {}, ammo: {}, vehicles: {}, gangs: {} },
+
+ // NEW: which tabs are allowed for this open instance
+ allowedTabs: ["weapons", "ammo", "vehicles"],
+ };
+
+ function fmtMoney(n) {
+ n = Number(n) || 0;
+ return "$" + n.toLocaleString();
+ }
+
+ function normalizeAllowedTabs(arr) {
+ // If not provided, allow all
+ if (!Array.isArray(arr) || arr.length === 0) return [...ALL_TABS];
+
+ // Keep only known tabs, unique, in ALL_TABS order
+ const set = new Set(arr.map(String));
+ return ALL_TABS.filter(t => set.has(t));
+ }
+
+ function firstAllowedTab() {
+ return (state.allowedTabs && state.allowedTabs[0]) ? state.allowedTabs[0] : "weapons";
+ }
+
+ function applyAllowedTabsUI() {
+ // Hide / show tab buttons
+ for (const t of ALL_TABS) {
+ const btn = tabBtns[t];
+ const ok = state.allowedTabs.includes(t);
+ btn.classList.toggle("hidden", !ok);
+ // Also prevent focusing hidden buttons
+ btn.tabIndex = ok ? 0 : -1;
+ }
+
+ // If current tab not allowed, force to first allowed
+ if (!state.allowedTabs.includes(state.tab)) {
+ state.tab = firstAllowedTab();
+ }
+ }
+
+ function setActiveTab(tab) {
+ tab = String(tab || "");
+
+ // Enforce allowed tabs
+ if (!state.allowedTabs.includes(tab)) {
+ tab = firstAllowedTab();
+ }
+
+ state.tab = tab;
+ Object.keys(tabBtns).forEach((k) => tabBtns[k].classList.toggle("active", k === tab));
+ renderList();
+ }
+
+ function renderList() {
+ list.innerHTML = "";
+
+ // Enforce allowed tab again (in case something changed)
+ if (!state.allowedTabs.includes(state.tab)) {
+ setActiveTab(firstAllowedTab());
+ return;
+ }
+
+ const catalog = state.payload[state.tab] || {};
+ const entries = Object.entries(catalog);
+
+ if (entries.length === 0) {
+ const empty = document.createElement("div");
+ empty.style.padding = "10px 0";
+ empty.style.opacity = "0.8";
+ empty.textContent = "No items configured.";
+ list.appendChild(empty);
+ return;
+ }
+
+ for (const [itemId, it] of entries) {
+ const row = document.createElement("div");
+ row.className = "shop-row";
+
+ const left = document.createElement("div");
+
+ const name = document.createElement("div");
+ name.className = "shop-item-name";
+ name.textContent = it.label || itemId;
+
+ const sub = document.createElement("div");
+ sub.className = "shop-item-sub";
+
+ if (state.tab === "vehicles" && itemId === "gangveh") {
+ const gName =
+ (state.payload.gangs &&
+ state.payload.gangs[state.gangId] &&
+ state.payload.gangs[state.gangId].name) ||
+ "Your Gang";
+ sub.textContent = `Free gang vehicle for ${gName}`;
+ } else {
+ sub.textContent = `ID: ${itemId}`;
+ }
+
+ left.appendChild(name);
+ left.appendChild(sub);
+
+ const right = document.createElement("div");
+ right.style.display = "flex";
+ right.style.alignItems = "center";
+
+ const price = document.createElement("div");
+ price.className = "shop-price";
+ price.textContent = fmtMoney(it.price || 0);
+
+ const buy = document.createElement("button");
+ buy.className = "shop-buy";
+ buy.type = "button";
+ buy.textContent = "Buy";
+ buy.onclick = () => doBuy(state.tab, itemId);
+
+ right.appendChild(price);
+ right.appendChild(buy);
+
+ row.appendChild(left);
+ row.appendChild(right);
+
+ list.appendChild(row);
+ }
+ }
+
+ function post(name, data) {
+ return fetch(`https://${GetParentResourceName()}/${name}`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json; charset=UTF-8" },
+ body: JSON.stringify(data || {}),
+ }).catch((err) => {
+ console.error("[turfwar] NUI post failed:", name, err);
+ });
+ }
+
+ function doBuy(tab, itemId) {
+ // Hard enforce allowed tabs (security-ish)
+ if (!state.allowedTabs.includes(tab)) {
+ console.warn("[turfwar] blocked buy from disallowed tab:", tab, itemId);
+ return;
+ }
+
+ if (tab === "weapons") return post("shop:buyWeapon", { itemId });
+ if (tab === "ammo") return post("shop:buyAmmo", { itemId });
+ if (tab === "vehicles") return post("shop:buyVehicle", { itemId });
+ }
+
+ function open(msg) {
+ state.open = true;
+
+ state.gangId = Number(msg.gangId) || 0;
+ state.payload = msg.payload || state.payload;
+
+ // NEW: allowed tabs
+ state.allowedTabs = normalizeAllowedTabs(msg.allowedTabs);
+
+ // requested tab (will be forced to allowed)
+ state.tab = String(msg.tab || "weapons");
+
+ title.textContent = "Gang Shop";
+ balanceNum.textContent = fmtMoney(state.balance);
+
+ // apply allowed tab UI before setting active tab
+ applyAllowedTabsUI();
+
+ wrap.classList.remove("hidden");
+ setActiveTab(state.tab);
+ }
+
+ function close() {
+ state.open = false;
+ wrap.classList.add("hidden");
+ post("shop:close", {});
+ }
+
+ // Buttons (they’ll be hidden when not allowed)
+ tabBtns.weapons.addEventListener("click", () => setActiveTab("weapons"));
+ tabBtns.ammo.addEventListener("click", () => setActiveTab("ammo"));
+ tabBtns.vehicles.addEventListener("click", () => setActiveTab("vehicles"));
+ closeBtn.addEventListener("click", close);
+
+ // ESC closes
+window.addEventListener("message", (e) => {
+ if (e.data.type === "shop:open") {
+ const rgb = e.data.gangRGB || { r: 10, g: 10, b: 10 }
+ document.documentElement.style.setProperty(
+ "--gang-rgb",
+ `${rgb.r}, ${rgb.g}, ${rgb.b}`
+ )
+ }
+})
+
+ // Receive messages from Lua
+ window.addEventListener("message", (event) => {
+ const msg = event.data;
+ if (!msg || !msg.type) return;
+
+ if (msg.type === "shop:open") {
+ console.log("[turfwar] shop:open", msg);
+ open(msg);
+
+ } else if (msg.type === "shop:close") {
+ wrap.classList.add("hidden");
+ state.open = false;
+
+ } else if (msg.type === "shop:balance") {
+ state.balance = Number(msg.balance) || 0;
+ state.gangId = Number(msg.gangId) || state.gangId;
+ balanceNum.textContent = fmtMoney(state.balance);
+
+ } else if (msg.type === "shop:gang") {
+ state.gangId = Number(msg.gangId) || 0;
+ renderList();
+ }
+ });
+})();