docs/website-pages-spec #5

Merged
archvillainette merged 2 commits from docs/website-pages-spec into main 2026-06-26 08:00:06 +00:00
39 changed files with 9143 additions and 0 deletions
+223
View File
@@ -0,0 +1,223 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<meta name="design_doc_mode" content="canvas">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=Jost:wght@300;400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;}
.lbl{font-family:'Jost',sans-serif;font-size:13px;letter-spacing:.04em;color:#3a3530;margin-bottom:12px;font-weight:500;}
.lbl b{color:#6b6152;font-weight:600;}
</style>
</helmet>
<!-- ============================================================= FRAME 1: DESKTOP · MEMBER -->
<div style="position:absolute;left:0px;top:0px;width:1300px;">
<div class="lbl" data-drags-parent="1"><b>Top bar</b> — Desktop · signed in (default). Brand strip links home.</div>
<div style="background:#0f0d12;border:1px solid rgba(194,163,90,0.14);border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,0.4);overflow:hidden;">
<div style="height:66px;display:flex;align-items:center;justify-content:space-between;padding:0 26px;border-bottom:1px solid rgba(194,163,90,0.16);background:linear-gradient(180deg,rgba(20,17,25,0.9),rgba(15,13,18,0.92));">
<div style="display:flex;align-items:center;gap:36px;">
<div style="display:flex;align-items:center;gap:12px;cursor:pointer;">
<div style="width:24px;height:24px;transform:rotate(45deg);border:1.5px solid #c2a35a;display:flex;align-items:center;justify-content:center;box-shadow:0 0 12px rgba(194,163,90,0.22);"><div style="width:8px;height:8px;background:#c2a35a;"></div></div>
<div style="font-family:'Cinzel',serif;font-weight:600;font-size:15px;letter-spacing:0.14em;color:#e6e0d6;white-space:nowrap;">SHADOWS OVER WESTGATE</div>
</div>
<div style="display:flex;align-items:center;gap:26px;">
<div style="position:relative;font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#e6e0d6;padding:4px 0;">News<span style="position:absolute;left:-3px;right:-3px;bottom:-22px;height:2px;background:linear-gradient(90deg,transparent,#c2a35a,transparent);"></span></div>
<div style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#b9b2a6;padding:4px 0;">Gallery</div>
<div style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#b9b2a6;padding:4px 0;">Wiki</div>
<div style="display:flex;align-items:center;gap:6px;font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#b9b2a6;padding:4px 0;">Forums<svg width="9" height="6" viewBox="0 0 9 6" fill="none"><path d="M1 1l3.5 3.5L8 1" stroke="#a8893f" stroke-width="1.4" stroke-linecap="round"/></svg></div>
<div style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#b9b2a6;padding:4px 0;white-space:nowrap;">Dev Blog</div>
</div>
</div>
<div style="display:flex;align-items:center;gap:16px;">
<div style="width:34px;height:34px;border-radius:6px;border:1px solid rgba(194,163,90,0.18);display:flex;align-items:center;justify-content:center;background:rgba(194,163,90,0.05);"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5" stroke="#b9b2a6" stroke-width="1.4"/><path d="M11 11l3 3" stroke="#b9b2a6" stroke-width="1.4" stroke-linecap="round"/></svg></div>
<div style="position:relative;width:34px;height:34px;display:flex;align-items:center;justify-content:center;"><svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M9 2a4 4 0 00-4 4c0 4-1.5 5-1.5 5h11S13 10 13 6a4 4 0 00-4-4z" stroke="#b9b2a6" stroke-width="1.3" stroke-linejoin="round"/><path d="M7.5 14a1.5 1.5 0 003 0" stroke="#b9b2a6" stroke-width="1.3"/></svg><span style="position:absolute;top:-2px;right:-2px;min-width:15px;height:15px;border-radius:8px;background:linear-gradient(180deg,#d8bd76,#a8893f);color:#150f08;font-family:'IBM Plex Mono',monospace;font-size:9px;font-weight:600;display:flex;align-items:center;justify-content:center;padding:0 3px;border:1px solid #0f0d12;">3</span></div>
<div style="position:relative;width:34px;height:34px;display:flex;align-items:center;justify-content:center;"><svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M3 4h12v8H7l-3 3v-3H3z" stroke="#b9b2a6" stroke-width="1.3" stroke-linejoin="round"/></svg><span style="position:absolute;top:-2px;right:-2px;min-width:15px;height:15px;border-radius:8px;background:linear-gradient(180deg,#d8bd76,#a8893f);color:#150f08;font-family:'IBM Plex Mono',monospace;font-size:9px;font-weight:600;display:flex;align-items:center;justify-content:center;padding:0 3px;border:1px solid #0f0d12;">2</span></div>
<div style="width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 34% 24%,rgba(255,238,190,0.22),transparent 30%),linear-gradient(145deg,#6f562d,#2c1122);border:1px solid rgba(216,194,138,0.4);box-shadow:0 0 0 2px #0f0d12,0 0 0 3px rgba(127,184,106,0.7);"></div>
</div>
</div>
</div>
</div>
<!-- ============================================================= FRAME 2: DESKTOP · GUEST -->
<div style="position:absolute;left:0px;top:170px;width:1300px;">
<div class="lbl" data-drags-parent="1"><b>Top bar</b> — Desktop · signed out (guest)</div>
<div style="background:#0f0d12;border:1px solid rgba(194,163,90,0.14);border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,0.4);overflow:hidden;">
<div style="height:66px;display:flex;align-items:center;justify-content:space-between;padding:0 26px;border-bottom:1px solid rgba(194,163,90,0.16);background:linear-gradient(180deg,rgba(20,17,25,0.9),rgba(15,13,18,0.92));">
<div style="display:flex;align-items:center;gap:36px;">
<div style="display:flex;align-items:center;gap:12px;cursor:pointer;">
<div style="width:24px;height:24px;transform:rotate(45deg);border:1.5px solid #c2a35a;display:flex;align-items:center;justify-content:center;box-shadow:0 0 12px rgba(194,163,90,0.22);"><div style="width:8px;height:8px;background:#c2a35a;"></div></div>
<div style="font-family:'Cinzel',serif;font-weight:600;font-size:15px;letter-spacing:0.14em;color:#e6e0d6;white-space:nowrap;">SHADOWS OVER WESTGATE</div>
</div>
<div style="display:flex;align-items:center;gap:26px;">
<div style="position:relative;font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#e6e0d6;padding:4px 0;">News<span style="position:absolute;left:-3px;right:-3px;bottom:-22px;height:2px;background:linear-gradient(90deg,transparent,#c2a35a,transparent);"></span></div>
<div style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#b9b2a6;padding:4px 0;">Gallery</div>
<div style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#b9b2a6;padding:4px 0;">Wiki</div>
<div style="display:flex;align-items:center;gap:6px;font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#b9b2a6;padding:4px 0;">Forums<svg width="9" height="6" viewBox="0 0 9 6" fill="none"><path d="M1 1l3.5 3.5L8 1" stroke="#a8893f" stroke-width="1.4" stroke-linecap="round"/></svg></div>
<div style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#b9b2a6;padding:4px 0;white-space:nowrap;">Dev Blog</div>
</div>
</div>
<div style="display:flex;align-items:center;gap:16px;">
<div style="width:34px;height:34px;border-radius:6px;border:1px solid rgba(194,163,90,0.18);display:flex;align-items:center;justify-content:center;background:rgba(194,163,90,0.05);"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5" stroke="#b9b2a6" stroke-width="1.4"/><path d="M11 11l3 3" stroke="#b9b2a6" stroke-width="1.4" stroke-linecap="round"/></svg></div>
<div style="font-family:'Jost',sans-serif;font-size:12px;letter-spacing:0.1em;text-transform:uppercase;color:#b9b2a6;">Log In</div>
<div style="font-family:'Jost',sans-serif;font-size:12px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d8bd76,#a8893f);padding:9px 20px;border-radius:6px;border:1px solid rgba(216,194,138,0.6);box-shadow:0 2px 12px rgba(168,133,74,0.3);">Register</div>
</div>
</div>
</div>
</div>
<!-- ============================================================= FRAME 3: FORUMS MENU OPEN -->
<div style="position:absolute;left:0px;top:340px;width:1300px;">
<div class="lbl" data-drags-parent="1"><b>Forums menu</b> — replaces the category sidebar (categories are a sample to edit later)</div>
<div style="background:#0f0d12;border:1px solid rgba(194,163,90,0.14);border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,0.4);overflow:hidden;padding-bottom:30px;">
<div style="height:66px;display:flex;align-items:center;justify-content:space-between;padding:0 26px;border-bottom:1px solid rgba(194,163,90,0.16);background:linear-gradient(180deg,rgba(20,17,25,0.9),rgba(15,13,18,0.92));">
<div style="display:flex;align-items:center;gap:36px;">
<div style="display:flex;align-items:center;gap:12px;cursor:pointer;">
<div style="width:24px;height:24px;transform:rotate(45deg);border:1.5px solid #c2a35a;display:flex;align-items:center;justify-content:center;"><div style="width:8px;height:8px;background:#c2a35a;"></div></div>
<div style="font-family:'Cinzel',serif;font-weight:600;font-size:15px;letter-spacing:0.14em;color:#e6e0d6;white-space:nowrap;">SHADOWS OVER WESTGATE</div>
</div>
<div style="display:flex;align-items:center;gap:26px;">
<div style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#b9b2a6;padding:4px 0;">News</div>
<div style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#b9b2a6;padding:4px 0;">Gallery</div>
<div style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#b9b2a6;padding:4px 0;">Wiki</div>
<div style="position:relative;display:flex;align-items:center;gap:6px;font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#e6e0d6;padding:4px 0;">Forums<svg width="9" height="6" viewBox="0 0 9 6" fill="none"><path d="M1 1l3.5 3.5L8 1" stroke="#c2a35a" stroke-width="1.4" stroke-linecap="round"/></svg><span style="position:absolute;left:-3px;right:14px;bottom:-22px;height:2px;background:linear-gradient(90deg,transparent,#c2a35a,transparent);"></span></div>
<div style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#b9b2a6;padding:4px 0;white-space:nowrap;">Dev Blog</div>
</div>
</div>
<div style="display:flex;align-items:center;gap:16px;opacity:0.5;">
<div style="width:34px;height:34px;border-radius:6px;border:1px solid rgba(194,163,90,0.18);"></div>
<div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(145deg,#6f562d,#2c1122);border:1px solid rgba(216,194,138,0.4);"></div>
</div>
</div>
<div style="margin:20px 0 0 290px;width:560px;background:linear-gradient(100deg,rgba(42,18,34,0.5),rgba(25,22,31,0.97) 30%,rgba(18,16,23,0.99));border:1px solid rgba(194,163,90,0.2);border-radius:8px;box-shadow:inset 0 1px 0 rgba(255,255,255,0.03),0 18px 40px rgba(0,0,0,0.5);padding:10px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;">
<div style="padding:13px 16px;border-radius:6px;">
<div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;margin-bottom:3px;">News</div>
<div style="font-family:'Jost',sans-serif;font-size:12.5px;color:#9a9086;">Official server announcements</div>
</div>
<div style="padding:13px 16px;border-radius:6px;">
<div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;margin-bottom:3px;">Developer Blog</div>
<div style="font-family:'Jost',sans-serif;font-size:12.5px;color:#9a9086;">Behind-the-scenes updates</div>
</div>
<div style="padding:13px 16px;border-radius:6px;background:rgba(194,163,90,0.06);">
<div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;margin-bottom:3px;">General Discussion</div>
<div style="font-family:'Jost',sans-serif;font-size:12.5px;color:#9a9086;">Talk of the city and beyond</div>
</div>
<div style="padding:13px 16px;border-radius:6px;">
<div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;margin-bottom:3px;">Roleplay & Stories</div>
<div style="font-family:'Jost',sans-serif;font-size:12.5px;color:#9a9086;">In-character journals and tales</div>
</div>
<div style="padding:13px 16px;border-radius:6px;">
<div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;margin-bottom:3px;">Guilds & Factions</div>
<div style="font-family:'Jost',sans-serif;font-size:12.5px;color:#9a9086;">Masked guilds and noble houses</div>
</div>
<div style="padding:13px 16px;border-radius:6px;">
<div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;margin-bottom:3px;">Help & Support</div>
<div style="font-family:'Jost',sans-serif;font-size:12.5px;color:#9a9086;">New players start here</div>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:8px;padding:14px 16px 6px;border-top:1px solid rgba(194,163,90,0.12);">
<div style="display:flex;gap:20px;">
<span style="font-family:'Jost',sans-serif;font-size:12px;letter-spacing:0.06em;text-transform:uppercase;color:#b9b2a6;">Recent</span>
<span style="font-family:'Jost',sans-serif;font-size:12px;letter-spacing:0.06em;text-transform:uppercase;color:#b9b2a6;">Unread</span>
<span style="font-family:'Jost',sans-serif;font-size:12px;letter-spacing:0.06em;text-transform:uppercase;color:#b9b2a6;">Tags</span>
</div>
<span style="font-family:'Jost',sans-serif;font-size:12px;letter-spacing:0.1em;text-transform:uppercase;color:#c2a35a;">All Categories →</span>
</div>
</div>
</div>
</div>
<!-- ============================================================= FRAME 4: USER MENU OPEN -->
<div style="position:absolute;left:0px;top:800px;width:1300px;">
<div class="lbl" data-drags-parent="1"><b>User menu</b> — profile, bookmarks, settings, sign out</div>
<div style="background:#0f0d12;border:1px solid rgba(194,163,90,0.14);border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,0.4);overflow:hidden;padding-bottom:30px;">
<div style="height:66px;display:flex;align-items:center;justify-content:space-between;padding:0 26px;border-bottom:1px solid rgba(194,163,90,0.16);background:linear-gradient(180deg,rgba(20,17,25,0.9),rgba(15,13,18,0.92));">
<div style="display:flex;align-items:center;gap:36px;opacity:0.55;">
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:24px;height:24px;transform:rotate(45deg);border:1.5px solid #c2a35a;display:flex;align-items:center;justify-content:center;"><div style="width:8px;height:8px;background:#c2a35a;"></div></div>
<div style="font-family:'Cinzel',serif;font-weight:600;font-size:15px;letter-spacing:0.14em;color:#e6e0d6;white-space:nowrap;">SHADOWS OVER WESTGATE</div>
</div>
</div>
<div style="display:flex;align-items:center;gap:16px;">
<div style="width:34px;height:34px;border-radius:6px;border:1px solid rgba(194,163,90,0.18);display:flex;align-items:center;justify-content:center;background:rgba(194,163,90,0.05);"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5" stroke="#b9b2a6" stroke-width="1.4"/><path d="M11 11l3 3" stroke="#b9b2a6" stroke-width="1.4" stroke-linecap="round"/></svg></div>
<div style="width:34px;height:34px;display:flex;align-items:center;justify-content:center;"><svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M9 2a4 4 0 00-4 4c0 4-1.5 5-1.5 5h11S13 10 13 6a4 4 0 00-4-4z" stroke="#b9b2a6" stroke-width="1.3" stroke-linejoin="round"/><path d="M7.5 14a1.5 1.5 0 003 0" stroke="#b9b2a6" stroke-width="1.3"/></svg></div>
<div style="width:34px;height:34px;display:flex;align-items:center;justify-content:center;"><svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M3 4h12v8H7l-3 3v-3H3z" stroke="#b9b2a6" stroke-width="1.3" stroke-linejoin="round"/></svg></div>
<div style="position:relative;width:36px;height:36px;border-radius:50%;background:radial-gradient(circle at 34% 24%,rgba(255,238,190,0.22),transparent 30%),linear-gradient(145deg,#6f562d,#2c1122);border:1px solid rgba(216,194,138,0.4);box-shadow:0 0 0 2px #0f0d12,0 0 0 3px rgba(127,184,106,0.7);"></div>
</div>
</div>
<div style="margin:14px 26px 0 auto;width:248px;background:linear-gradient(100deg,rgba(42,18,34,0.5),rgba(25,22,31,0.97) 30%,rgba(18,16,23,0.99));border:1px solid rgba(194,163,90,0.2);border-radius:8px;box-shadow:inset 0 1px 0 rgba(255,255,255,0.03),0 18px 40px rgba(0,0,0,0.5);overflow:hidden;">
<div style="display:flex;align-items:center;gap:12px;padding:16px 18px;border-bottom:1px solid rgba(194,163,90,0.12);">
<div style="width:40px;height:40px;border-radius:50%;background:radial-gradient(circle at 34% 24%,rgba(255,238,190,0.22),transparent 30%),linear-gradient(145deg,#6f562d,#2c1122);border:1px solid rgba(216,194,138,0.4);flex:none;"></div>
<div style="min-width:0;">
<div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">archvillainette</div>
<div style="font-family:'Jost',sans-serif;font-size:12px;color:#c2a35a;">View profile</div>
</div>
</div>
<div style="padding:8px;">
<div style="display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:6px;font-family:'Jost',sans-serif;font-size:14px;color:#b9b2a6;"><svg width="15" height="15" viewBox="0 0 15 15" fill="none"><circle cx="7.5" cy="5" r="2.6" stroke="#a8893f" stroke-width="1.3"/><path d="M2.5 13c0-2.5 2.2-4 5-4s5 1.5 5 4" stroke="#a8893f" stroke-width="1.3"/></svg>Profile</div>
<div style="display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:6px;font-family:'Jost',sans-serif;font-size:14px;color:#b9b2a6;"><svg width="15" height="15" viewBox="0 0 15 15" fill="none"><path d="M4 2h7v11l-3.5-2.2L4 13z" stroke="#a8893f" stroke-width="1.3" stroke-linejoin="round"/></svg>Bookmarks</div>
<div style="display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:6px;background:rgba(194,163,90,0.06);font-family:'Jost',sans-serif;font-size:14px;color:#e6e0d6;"><svg width="15" height="15" viewBox="0 0 15 15" fill="none"><circle cx="7.5" cy="7.5" r="2.4" stroke="#c2a35a" stroke-width="1.3"/><path d="M7.5 1.5v2M7.5 11.5v2M1.5 7.5h2M11.5 7.5h2" stroke="#c2a35a" stroke-width="1.3" stroke-linecap="round"/></svg>Settings</div>
<div style="height:1px;background:rgba(194,163,90,0.12);margin:7px 4px;"></div>
<div style="display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:6px;font-family:'Jost',sans-serif;font-size:14px;color:#bf8a86;"><svg width="15" height="15" viewBox="0 0 15 15" fill="none"><path d="M6 2H3v11h3M9.5 4.5L12.5 7.5 9.5 10.5M12 7.5H6" stroke="#bf8a86" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>Sign Out</div>
</div>
</div>
</div>
</div>
<!-- ============================================================= FRAME 5: MOBILE -->
<div style="position:absolute;left:1360px;top:0px;width:392px;">
<div class="lbl" data-drags-parent="1"><b>Mobile</b> — compact bar</div>
<div style="width:390px;background:#0f0d12;border:1px solid rgba(194,163,90,0.14);border-radius:14px;box-shadow:0 1px 3px rgba(0,0,0,0.4);overflow:hidden;">
<div style="height:58px;display:flex;align-items:center;justify-content:space-between;padding:0 16px;border-bottom:1px solid rgba(194,163,90,0.16);background:linear-gradient(180deg,rgba(20,17,25,0.9),rgba(15,13,18,0.92));">
<div style="display:flex;flex-direction:column;gap:4px;cursor:pointer;"><span style="width:20px;height:1.6px;background:#c2a35a;display:block;"></span><span style="width:20px;height:1.6px;background:#c2a35a;display:block;"></span><span style="width:20px;height:1.6px;background:#c2a35a;display:block;"></span></div>
<div style="display:flex;align-items:center;gap:9px;">
<div style="width:18px;height:18px;transform:rotate(45deg);border:1.4px solid #c2a35a;display:flex;align-items:center;justify-content:center;"><div style="width:6px;height:6px;background:#c2a35a;"></div></div>
<div style="font-family:'Cinzel',serif;font-weight:600;font-size:12px;letter-spacing:0.1em;color:#e6e0d6;">WESTGATE</div>
</div>
<div style="display:flex;align-items:center;gap:12px;">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5" stroke="#b9b2a6" stroke-width="1.4"/><path d="M11 11l3 3" stroke="#b9b2a6" stroke-width="1.4" stroke-linecap="round"/></svg>
<div style="width:30px;height:30px;border-radius:50%;background:linear-gradient(145deg,#6f562d,#2c1122);border:1px solid rgba(216,194,138,0.4);box-shadow:0 0 0 2px #0f0d12,0 0 0 3px rgba(127,184,106,0.7);"></div>
</div>
</div>
</div>
</div>
<!-- ============================================================= FRAME 6: MOBILE DRAWER -->
<div style="position:absolute;left:1360px;top:150px;width:392px;">
<div class="lbl" data-drags-parent="1"><b>Mobile</b> — drawer open</div>
<div style="width:390px;background:#0f0d12;border:1px solid rgba(194,163,90,0.14);border-radius:14px;box-shadow:0 1px 3px rgba(0,0,0,0.4);overflow:hidden;">
<div style="height:58px;display:flex;align-items:center;justify-content:space-between;padding:0 16px;border-bottom:1px solid rgba(194,163,90,0.16);background:linear-gradient(180deg,rgba(20,17,25,0.9),rgba(15,13,18,0.92));">
<div style="display:flex;flex-direction:column;gap:5px;"><span style="width:20px;height:1.6px;background:#c2a35a;display:block;transform:rotate(45deg) translateY(0px);"></span><span style="width:20px;height:1.6px;background:#c2a35a;display:block;transform:rotate(-45deg);margin-top:-7px;"></span></div>
<div style="display:flex;align-items:center;gap:9px;">
<div style="width:18px;height:18px;transform:rotate(45deg);border:1.4px solid #c2a35a;display:flex;align-items:center;justify-content:center;"><div style="width:6px;height:6px;background:#c2a35a;"></div></div>
<div style="font-family:'Cinzel',serif;font-weight:600;font-size:12px;letter-spacing:0.1em;color:#e6e0d6;">WESTGATE</div>
</div>
<div style="width:30px;height:30px;border-radius:50%;background:linear-gradient(145deg,#6f562d,#2c1122);border:1px solid rgba(216,194,138,0.4);"></div>
</div>
<div style="padding:10px 14px 18px;">
<div style="display:flex;align-items:center;gap:10px;background:rgba(8,7,10,0.5);border:1px solid rgba(194,163,90,0.18);border-radius:8px;padding:10px 12px;margin:8px 0 14px;"><svg width="15" height="15" viewBox="0 0 15 15" fill="none"><circle cx="6.5" cy="6.5" r="4.5" stroke="#9a9086" stroke-width="1.3"/><path d="M10 10l3 3" stroke="#9a9086" stroke-width="1.3" stroke-linecap="round"/></svg><span style="font-family:'Jost',sans-serif;font-size:13px;color:#9a9086;">Search Westgate…</span></div>
<div style="font-family:'Jost',sans-serif;font-size:15px;color:#e6e0d6;padding:12px 8px;border-bottom:1px solid rgba(194,163,90,0.08);">News</div>
<div style="font-family:'Jost',sans-serif;font-size:15px;color:#b9b2a6;padding:12px 8px;border-bottom:1px solid rgba(194,163,90,0.08);">Gallery</div>
<div style="font-family:'Jost',sans-serif;font-size:15px;color:#b9b2a6;padding:12px 8px;border-bottom:1px solid rgba(194,163,90,0.08);">Wiki</div>
<div style="display:flex;align-items:center;justify-content:space-between;font-family:'Jost',sans-serif;font-size:15px;color:#b9b2a6;padding:12px 8px;border-bottom:1px solid rgba(194,163,90,0.08);">Forums<svg width="9" height="6" viewBox="0 0 9 6" fill="none"><path d="M1 1l3.5 3.5L8 1" stroke="#a8893f" stroke-width="1.4" stroke-linecap="round"/></svg></div>
<div style="font-family:'Jost',sans-serif;font-size:15px;color:#b9b2a6;padding:12px 8px;border-bottom:1px solid rgba(194,163,90,0.08);">Dev Blog</div>
<div style="display:flex;align-items:center;gap:18px;margin-top:18px;padding:0 8px;">
<div style="position:relative;display:flex;align-items:center;gap:8px;font-family:'Jost',sans-serif;font-size:13px;color:#b9b2a6;"><svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M9 2a4 4 0 00-4 4c0 4-1.5 5-1.5 5h11S13 10 13 6a4 4 0 00-4-4z" stroke="#b9b2a6" stroke-width="1.3" stroke-linejoin="round"/><path d="M7.5 14a1.5 1.5 0 003 0" stroke="#b9b2a6" stroke-width="1.3"/></svg>Notifications<span style="width:7px;height:7px;border-radius:50%;background:#c2a35a;"></span></div>
<div style="display:flex;align-items:center;gap:8px;font-family:'Jost',sans-serif;font-size:13px;color:#b9b2a6;"><svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M3 4h12v8H7l-3 3v-3H3z" stroke="#b9b2a6" stroke-width="1.3" stroke-linejoin="round"/></svg>Messages</div>
</div>
</div>
</div>
</div>
</x-dc>
</body>
</html>
@@ -0,0 +1,563 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=Jost:wght@300;400;500;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;}
body{margin:0;background:#080509;}
::selection{background:rgba(201,168,106,0.28);color:#f3ead9;}
@keyframes fadeUp{from{opacity:0;transform:translateY(14px);}to{opacity:1;transform:translateY(0);}}
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(120,190,120,0.5);}70%{box-shadow:0 0 0 9px rgba(120,190,120,0);}100%{box-shadow:0 0 0 0 rgba(120,190,120,0);}}
@keyframes drift{from{background-position:0 0;}to{background-position:60px 60px;}}
a{text-decoration:none;color:inherit;}
.wg-scroll::-webkit-scrollbar{width:0;height:0;}
</style>
</helmet>
<div style="min-height:100vh;font-family:'Jost',sans-serif;color:#cabfae;background:radial-gradient(125% 80% at 50% -5%, #1c1428 0%, #100b16 34%, #0a0710 66%, #070409 100%);background-attachment:fixed;overflow-x:hidden;">
<!-- ================= NAV ================= -->
<nav style="position:sticky;top:0;z-index:50;backdrop-filter:blur(14px);background:rgba(9,6,12,0.82);border-bottom:1px solid rgba(201,168,106,0.16);">
<div style="max-width:1240px;margin:0 auto;padding:0 32px;height:66px;display:flex;align-items:center;justify-content:space-between;gap:24px;">
<div onClick="{{ goHome }}" style="display:flex;align-items:center;gap:13px;cursor:pointer;">
<div style="width:26px;height:26px;transform:rotate(45deg);border:1.5px solid #c9a86a;display:flex;align-items:center;justify-content:center;box-shadow:0 0 14px rgba(201,168,106,0.25);">
<div style="width:9px;height:9px;background:#c9a86a;"></div>
</div>
<div style="font-family:'Cinzel',serif;font-weight:600;font-size:16px;letter-spacing:0.16em;color:#e8dcc6;white-space:nowrap;">SHADOWS OVER WESTGATE</div>
</div>
<div style="display:flex;align-items:center;gap:30px;">
<div onClick="{{ goHome }}" style="position:relative;cursor:pointer;font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;" style-hover="color:#e8dcc6;">Home
<sc-if value="{{ isHome }}" hint-placeholder-val="{{ true }}"><span style="position:absolute;left:-4px;right:-4px;bottom:-21px;height:2px;background:linear-gradient(90deg,transparent,#c9a86a,transparent);"></span></sc-if>
</div>
<div onClick="{{ goNews }}" style="position:relative;cursor:pointer;font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;" style-hover="color:#e8dcc6;">News
<sc-if value="{{ isNews }}" hint-placeholder-val="{{ false }}"><span style="position:absolute;left:-4px;right:-4px;bottom:-21px;height:2px;background:linear-gradient(90deg,transparent,#c9a86a,transparent);"></span></sc-if>
</div>
<a href="https://westgate.pw/categories" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;" style-hover="color:#e8dcc6;">Forums</a>
<a href="https://westgate.pw/wiki" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;" style-hover="color:#e8dcc6;">Wiki</a>
<div onClick="{{ goGallery }}" style="position:relative;cursor:pointer;font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;" style-hover="color:#e8dcc6;">Gallery
<sc-if value="{{ isGallery }}" hint-placeholder-val="{{ false }}"><span style="position:absolute;left:-4px;right:-4px;bottom:-21px;height:2px;background:linear-gradient(90deg,transparent,#c9a86a,transparent);"></span></sc-if>
</div>
<div onClick="{{ goBlog }}" style="position:relative;cursor:pointer;font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;white-space:nowrap;" style-hover="color:#e8dcc6;">Dev Blog
<sc-if value="{{ isBlog }}" hint-placeholder-val="{{ false }}"><span style="position:absolute;left:-4px;right:-4px;bottom:-21px;height:2px;background:linear-gradient(90deg,transparent,#c9a86a,transparent);"></span></sc-if>
</div>
</div>
<div style="display:flex;align-items:center;gap:14px;">
<a href="https://westgate.pw/login" style="font-family:'Inter',sans-serif;font-size:12px;letter-spacing:0.1em;text-transform:uppercase;color:#c4b8a6;white-space:nowrap;" style-hover="color:#e8dcc6;">Log In</a>
<a href="https://westgate.pw/register" style="font-family:'Inter',sans-serif;font-size:12px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:9px 18px;border-radius:3px;border:1px solid #ddc488;box-shadow:0 2px 12px rgba(168,133,74,0.3);">Register</a>
</div>
</div>
</nav>
<!-- ================= HOME ================= -->
<sc-if value="{{ isHome }}" hint-placeholder-val="{{ true }}">
<div style="animation:fadeUp .5s ease both;">
<!-- HERO -->
<section style="position:relative;height:560px;border-bottom:1px solid rgba(201,168,106,0.16);overflow:hidden;">
<div style="position:absolute;inset:0;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-hero" shape="rect" placeholder="Drop banner key art" style="width:100%;height:100%;" hint-size="100%,100%"></x-import>
</div>
<div style="position:absolute;inset:0;pointer-events:none;background:linear-gradient(180deg,rgba(10,7,14,0.55) 0%,rgba(10,7,14,0.2) 35%,rgba(10,7,14,0.78) 82%,#0a0710 100%);"></div>
<div style="position:absolute;inset:0;pointer-events:none;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:0 24px;">
<div style="font-family:'Cinzel',serif;font-size:13px;letter-spacing:0.42em;text-transform:uppercase;color:#c9a86a;margin-bottom:22px;">Neverwinter Nights · Persistent World</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:72px;line-height:1.02;letter-spacing:0.03em;color:#f1e8d6;margin:0;text-shadow:0 4px 40px rgba(0,0,0,0.7);">Welcome to Westgate</h1>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:20px;color:#cdc1b0;max-width:620px;margin:24px 0 0;line-height:1.6;text-shadow:0 2px 16px rgba(0,0,0,0.8);">An ancient port city rises out of the mists, at the edge of the Sea of Fallen Stars. Mind the shadows.</p>
<div style="display:flex;gap:16px;margin-top:38px;pointer-events:auto;">
<a href="https://westgate.pw/wiki" style="font-family:'Inter',sans-serif;font-size:13px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:15px 32px;border-radius:3px;border:1px solid #ddc488;box-shadow:0 4px 22px rgba(168,133,74,0.35);">How to Join</a>
<div onClick="{{ goNews }}" style="cursor:pointer;font-family:'Inter',sans-serif;font-size:13px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#e2d3b4;background:rgba(20,14,26,0.55);backdrop-filter:blur(6px);padding:15px 32px;border-radius:3px;border:1px solid rgba(201,168,106,0.4);">Latest News</div>
</div>
</div>
</section>
<!-- SERVER STATUS -->
<section style="max-width:1240px;margin:0 auto;padding:0 32px;">
<div style="margin-top:-44px;position:relative;z-index:10;background:linear-gradient(160deg,#1a1322,#100b18);border:1px solid rgba(201,168,106,0.2);border-radius:5px;box-shadow:0 24px 60px rgba(0,0,0,0.5);padding:26px 34px;display:flex;align-items:center;gap:40px;flex-wrap:wrap;">
<div style="display:flex;align-items:center;gap:16px;">
<div style="width:13px;height:13px;border-radius:50%;background:#7fc07f;animation:pulse 2.4s infinite;"></div>
<div>
<div style="font-family:'Cinzel',serif;font-size:18px;letter-spacing:0.06em;color:#e8dcc6;">Server Online</div>
<div style="font-family:'Inter',sans-serif;font-size:12px;letter-spacing:0.08em;color:#8a7e6d;margin-top:3px;">Alpha · Module 0.3</div>
</div>
</div>
<div style="width:1px;height:42px;background:rgba(201,168,106,0.18);"></div>
<div>
<div style="font-family:'Cinzel',serif;font-size:26px;color:#e8dcc6;">14 <span style="font-size:15px;color:#8a7e6d;">souls online</span></div>
<div style="font-family:'Inter',sans-serif;font-size:12px;letter-spacing:0.08em;color:#8a7e6d;margin-top:2px;">Peaked 32 today</div>
</div>
<div style="width:1px;height:42px;background:rgba(201,168,106,0.18);"></div>
<div style="flex:1;min-width:180px;">
<div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.16em;text-transform:uppercase;color:#c9a86a;">Direct Connect</div>
<div style="font-family:'Inter',sans-serif;font-size:15px;color:#d8cdbb;margin-top:4px;">play.westgate.pw</div>
</div>
<a href="https://westgate.pw/wiki" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:13px 26px;border-radius:3px;border:1px solid #ddc488;">Connect Guide</a>
</div>
</section>
<!-- PITCH -->
<section style="max-width:760px;margin:0 auto;padding:96px 32px 30px;text-align:center;">
<div style="display:flex;align-items:center;justify-content:center;gap:18px;margin-bottom:40px;">
<div style="height:1px;width:80px;background:linear-gradient(90deg,transparent,#c9a86a);"></div>
<div style="width:7px;height:7px;background:#c9a86a;transform:rotate(45deg);"></div>
<div style="height:1px;width:80px;background:linear-gradient(90deg,#c9a86a,transparent);"></div>
</div>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:23px;line-height:1.7;color:#ddd1bf;margin:0 0 34px;">Long ago, merchants from distant lands filled its harbors, and gold flowed as freely as wine — but time, as it does, has a way of curdling mortal splendor.</p>
<p style="font-family:'Jost',sans-serif;font-size:17px;line-height:1.95;color:#a99d8c;margin:0 0 26px;">Today its bones are old stone and salt-worn docks, and bargains are struck beneath every lantern's glow. Here, hoarded secrets, owed favour, and unspoken truths are the true measure of power — although coin certainly helps.</p>
<p style="font-family:'Jost',sans-serif;font-size:17px;line-height:1.95;color:#a99d8c;margin:0 0 26px;">Westgate is a city ever-balanced on a blade's edge, built upon decaying decadence. The masked and nameless guilds clot the veins of the city with their greed and cruelty. Noble houses cling to fading legacies as their bloodlines tangle with curses, ambition, and desperation. Old cults and buried powers hold the Undergate by its throat. Beneath it all, antediluvian things begin to stir — patient, watchful, and hungry.</p>
<blockquote style="font-family:'Cinzel',serif;font-style:italic;font-size:22px;line-height:1.5;color:#d9bd86;margin:46px auto;max-width:560px;position:relative;">
<span style="position:absolute;left:50%;top:-34px;transform:translateX(-50%);font-size:54px;color:rgba(201,168,106,0.3);font-family:'Cinzel',serif;"></span>
To walk Westgate's streets is to wager your soul in a game of chance.
</blockquote>
<p style="font-family:'Jost',sans-serif;font-size:17px;line-height:1.95;color:#a99d8c;margin:0 0 26px;">Every alley offers opportunity, yet each carries a price. While some come seeking fortune or power, others arrive hoping to disappear — and indeed they do, for in these boulevards you are lucky to merely survive. In Westgate, few leave unchanged… and fewer still leave at all.</p>
<p style="font-family:'Cinzel',serif;font-size:19px;letter-spacing:0.04em;color:#e8dcc6;margin:34px 0 0;">Welcome to Westgate. Mind the shadows.</p>
</section>
<!-- GET STARTED -->
<section style="max-width:1240px;margin:0 auto;padding:70px 32px 30px;">
<div style="text-align:center;margin-bottom:46px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c9a86a;margin-bottom:12px;">Your First Steps</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:38px;color:#ece2d2;margin:0;letter-spacing:0.03em;">Enter the City</h2>
</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:20px;">
<a href="https://westgate.pw/wiki" style="display:block;background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;padding:30px 26px;transition:all .25s;" style-hover="border:1px solid rgba(201,168,106,0.4);transform:translateY(-4px);">
<div style="width:42px;height:42px;border:1px solid rgba(201,168,106,0.4);border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:22px;"><div style="width:13px;height:13px;border:1.5px solid #c9a86a;transform:rotate(45deg);"></div></div>
<div style="font-family:'Cinzel',serif;font-size:19px;color:#e8dcc6;margin-bottom:10px;">How to Join</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#998d7c;margin:0;">Install Neverwinter Nights: EE, point it at our server, and step through the gate.</p>
</a>
<a href="https://westgate.pw/wiki" style="display:block;background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;padding:30px 26px;transition:all .25s;" style-hover="border:1px solid rgba(201,168,106,0.4);transform:translateY(-4px);">
<div style="width:42px;height:42px;border:1px solid rgba(201,168,106,0.4);border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:22px;"><div style="width:13px;height:13px;border:1.5px solid #c9a86a;"></div></div>
<div style="font-family:'Cinzel',serif;font-size:19px;color:#e8dcc6;margin-bottom:10px;">New Player Wiki</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#998d7c;margin:0;">Starter guides, character creation, and everything you need for your first night.</p>
</a>
<a href="https://westgate.pw/categories" style="display:block;background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;padding:30px 26px;transition:all .25s;" style-hover="border:1px solid rgba(201,168,106,0.4);transform:translateY(-4px);">
<div style="width:42px;height:42px;border:1px solid rgba(201,168,106,0.4);border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:22px;"><div style="width:15px;height:11px;border:1.5px solid #c9a86a;border-radius:2px;"></div></div>
<div style="font-family:'Cinzel',serif;font-size:19px;color:#e8dcc6;margin-bottom:10px;">The Setting Bible</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#998d7c;margin:0;">Lore of Westgate — its districts, guilds, noble houses, and buried histories.</p>
</a>
<a href="https://westgate.pw/categories" style="display:block;background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;padding:30px 26px;transition:all .25s;" style-hover="border:1px solid rgba(201,168,106,0.4);transform:translateY(-4px);">
<div style="width:42px;height:42px;border:1px solid rgba(201,168,106,0.4);border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:22px;"><div style="width:11px;height:11px;border-radius:50%;border:1.5px solid #c9a86a;"></div></div>
<div style="font-family:'Cinzel',serif;font-size:19px;color:#e8dcc6;margin-bottom:10px;">Join the Community</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#998d7c;margin:0;">Introduce yourself in the forums and find your place among Westgate's denizens.</p>
</a>
</div>
</section>
<!-- LATEST NEWS STRIP -->
<section style="max-width:1240px;margin:0 auto;padding:70px 32px 100px;">
<div style="display:flex;align-items:flex-end;justify-content:space-between;margin-bottom:34px;">
<div>
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c9a86a;margin-bottom:10px;">From the Watch</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:34px;color:#ece2d2;margin:0;letter-spacing:0.03em;">Latest News</h2>
</div>
<div onClick="{{ goNews }}" style="cursor:pointer;font-family:'Inter',sans-serif;font-size:12px;letter-spacing:0.12em;text-transform:uppercase;color:#c9a86a;border-bottom:1px solid rgba(201,168,106,0.4);padding-bottom:3px;">All Dispatches →</div>
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:22px;">
<div onClick="{{ goNews }}" style="cursor:pointer;background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;overflow:hidden;transition:all .25s;" style-hover="border:1px solid rgba(201,168,106,0.4);">
<div style="height:160px;background:repeating-linear-gradient(135deg,#1c1526,#1c1526 11px,#181120 11px,#181120 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(201,168,106,0.12);"><span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;color:#5e5446;text-transform:uppercase;">news image</span></div>
<div style="padding:24px;">
<div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#8a7e6d;margin-bottom:10px;">24 Jun 2026 · Announcement</div>
<div style="font-family:'Cinzel',serif;font-size:19px;line-height:1.35;color:#e8dcc6;margin-bottom:10px;">The Gates Open — Alpha Has Begun</div>
<p style="font-family:'Jost',sans-serif;font-size:14px;line-height:1.7;color:#998d7c;margin:0;">Westgate opens its harbour to its first wave of adventurers. Here is everything you need to know.</p>
</div>
</div>
<div onClick="{{ goNews }}" style="cursor:pointer;background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;overflow:hidden;transition:all .25s;" style-hover="border:1px solid rgba(201,168,106,0.4);">
<div style="height:160px;background:repeating-linear-gradient(135deg,#1c1526,#1c1526 11px,#181120 11px,#181120 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(201,168,106,0.12);"><span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;color:#5e5446;text-transform:uppercase;">news image</span></div>
<div style="padding:24px;">
<div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#8a7e6d;margin-bottom:10px;">21 Jun 2026 · Patch Note</div>
<div style="font-family:'Cinzel',serif;font-size:19px;line-height:1.35;color:#e8dcc6;margin-bottom:10px;">Patch 0.3 — The Undergate</div>
<p style="font-family:'Jost',sans-serif;font-size:14px;line-height:1.7;color:#998d7c;margin:0;">The tunnels beneath the city are now open. New areas, factions, and dangers await below.</p>
</div>
</div>
<div onClick="{{ goNews }}" style="cursor:pointer;background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;overflow:hidden;transition:all .25s;" style-hover="border:1px solid rgba(201,168,106,0.4);">
<div style="height:160px;background:repeating-linear-gradient(135deg,#1c1526,#1c1526 11px,#181120 11px,#181120 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(201,168,106,0.12);"><span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;color:#5e5446;text-transform:uppercase;">news image</span></div>
<div style="padding:24px;">
<div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#8a7e6d;margin-bottom:10px;">18 Jun 2026 · Community</div>
<div style="font-family:'Cinzel',serif;font-size:19px;line-height:1.35;color:#e8dcc6;margin-bottom:10px;">The Community Charter</div>
<p style="font-family:'Jost',sans-serif;font-size:14px;line-height:1.7;color:#998d7c;margin:0;">Our revised rules and the principles that keep Westgate's streets worth walking.</p>
</div>
</div>
</div>
</section>
</div>
</sc-if>
<!-- ================= NEWS ================= -->
<sc-if value="{{ isNews }}" hint-placeholder-val="{{ false }}">
<div style="animation:fadeUp .5s ease both;max-width:1100px;margin:0 auto;padding:72px 32px 100px;">
<div style="text-align:center;margin-bottom:56px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.3em;text-transform:uppercase;color:#c9a86a;margin-bottom:14px;">Dispatches from the Watch</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:54px;color:#f1e8d6;margin:0;letter-spacing:0.03em;">News</h1>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:18px;color:#a99d8c;margin:18px auto 0;max-width:520px;">Official server announcements, patch notes, and word from those who keep the gates.</p>
</div>
<!-- featured -->
<div style="background:linear-gradient(160deg,#1a1322,#100b18);border:1px solid rgba(201,168,106,0.2);border-radius:6px;overflow:hidden;display:grid;grid-template-columns:1.15fr 1fr;margin-bottom:46px;box-shadow:0 20px 50px rgba(0,0,0,0.4);">
<div style="position:relative;min-height:320px;background:repeating-linear-gradient(135deg,#221931,#221931 13px,#1b1428 13px,#1b1428 26px);display:flex;align-items:center;justify-content:center;">
<span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;color:#5e5446;text-transform:uppercase;">feature image</span>
</div>
<div style="padding:46px 44px;display:flex;flex-direction:column;justify-content:center;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:18px;">
<span style="font-family:'Inter',sans-serif;font-size:10.5px;letter-spacing:0.12em;text-transform:uppercase;color:#150f08;background:#c9a86a;padding:4px 11px;border-radius:2px;font-weight:600;">Announcement</span>
<span style="font-family:'Inter',sans-serif;font-size:12px;letter-spacing:0.06em;color:#8a7e6d;">24 June 2026</span>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:31px;line-height:1.2;color:#f1e8d6;margin:0 0 18px;">The Gates of Westgate Open — Alpha Has Begun</h2>
<p style="font-family:'Jost',sans-serif;font-size:16px;line-height:1.85;color:#a99d8c;margin:0 0 26px;">After long months in the shipyards, the harbour is open. Our alpha welcomes its first denizens to walk the salt-worn docks, strike bargains beneath the lanterns, and write the first chapters of Westgate's living history. Read on for what to expect, how to join, and where to begin.</p>
<div style="display:flex;align-items:center;gap:14px;">
<a href="https://westgate.pw/category/1/news" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:12px 26px;border-radius:3px;border:1px solid #ddc488;">Read Dispatch</a>
<span style="font-family:'Inter',sans-serif;font-size:12px;color:#8a7e6d;">by The Watch · 4 min read</span>
</div>
</div>
</div>
<!-- list -->
<div style="display:flex;flex-direction:column;">
<a href="https://westgate.pw/category/1/news" style="display:grid;grid-template-columns:130px 1fr auto;gap:28px;align-items:center;padding:26px 8px;border-top:1px solid rgba(201,168,106,0.12);transition:background .2s;" style-hover="background:rgba(201,168,106,0.04);">
<div style="text-align:center;"><div style="font-family:'Cinzel',serif;font-size:30px;color:#d9bd86;line-height:1;">21</div><div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#8a7e6d;margin-top:5px;">Jun 2026</div></div>
<div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:7px;"><span style="font-family:'Inter',sans-serif;font-size:10px;letter-spacing:0.12em;text-transform:uppercase;color:#9f86b8;border:1px solid rgba(159,134,184,0.4);padding:2px 8px;border-radius:2px;">Patch Note</span></div>
<div style="font-family:'Cinzel',serif;font-size:21px;color:#e8dcc6;margin-bottom:6px;">Patch 0.3 — The Undergate Expansion</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.6;color:#998d7c;margin:0;">New sub-levels, the Quivering Thumb questline, balance passes to thievery, and a reworked lantern lighting system.</p>
</div>
<div style="font-family:'Inter',sans-serif;font-size:22px;color:#6b6151;"></div>
</a>
<a href="https://westgate.pw/category/1/news" style="display:grid;grid-template-columns:130px 1fr auto;gap:28px;align-items:center;padding:26px 8px;border-top:1px solid rgba(201,168,106,0.12);transition:background .2s;" style-hover="background:rgba(201,168,106,0.04);">
<div style="text-align:center;"><div style="font-family:'Cinzel',serif;font-size:30px;color:#d9bd86;line-height:1;">18</div><div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#8a7e6d;margin-top:5px;">Jun 2026</div></div>
<div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:7px;"><span style="font-family:'Inter',sans-serif;font-size:10px;letter-spacing:0.12em;text-transform:uppercase;color:#c9a86a;border:1px solid rgba(201,168,106,0.4);padding:2px 8px;border-radius:2px;">Community</span></div>
<div style="font-family:'Cinzel',serif;font-size:21px;color:#e8dcc6;margin-bottom:6px;">The Community Charter & Rules Revision</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.6;color:#998d7c;margin:0;">An update to our code of conduct and roleplay standards. Please review before your next session.</p>
</div>
<div style="font-family:'Inter',sans-serif;font-size:22px;color:#6b6151;"></div>
</a>
<a href="https://westgate.pw/category/1/news" style="display:grid;grid-template-columns:130px 1fr auto;gap:28px;align-items:center;padding:26px 8px;border-top:1px solid rgba(201,168,106,0.12);transition:background .2s;" style-hover="background:rgba(201,168,106,0.04);">
<div style="text-align:center;"><div style="font-family:'Cinzel',serif;font-size:30px;color:#d9bd86;line-height:1;">12</div><div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#8a7e6d;margin-top:5px;">Jun 2026</div></div>
<div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:7px;"><span style="font-family:'Inter',sans-serif;font-size:10px;letter-spacing:0.12em;text-transform:uppercase;color:#150f08;background:#c9a86a;padding:2px 8px;border-radius:2px;font-weight:600;">Announcement</span></div>
<div style="font-family:'Cinzel',serif;font-size:21px;color:#e8dcc6;margin-bottom:6px;">New Player Onboarding: The Watch Welcomes You</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.6;color:#998d7c;margin:0;">A streamlined path from download to your first night in the city — mentors now available on arrival.</p>
</div>
<div style="font-family:'Inter',sans-serif;font-size:22px;color:#6b6151;"></div>
</a>
<a href="https://westgate.pw/category/1/news" style="display:grid;grid-template-columns:130px 1fr auto;gap:28px;align-items:center;padding:26px 8px;border-top:1px solid rgba(201,168,106,0.12);border-bottom:1px solid rgba(201,168,106,0.12);transition:background .2s;" style-hover="background:rgba(201,168,106,0.04);">
<div style="text-align:center;"><div style="font-family:'Cinzel',serif;font-size:30px;color:#d9bd86;line-height:1;">06</div><div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#8a7e6d;margin-top:5px;">Jun 2026</div></div>
<div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:7px;"><span style="font-family:'Inter',sans-serif;font-size:10px;letter-spacing:0.12em;text-transform:uppercase;color:#9f86b8;border:1px solid rgba(159,134,184,0.4);padding:2px 8px;border-radius:2px;">Maintenance</span></div>
<div style="font-family:'Cinzel',serif;font-size:21px;color:#e8dcc6;margin-bottom:6px;">Scheduled Module Reset — June 28</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.6;color:#998d7c;margin:0;">The server will be briefly unavailable for a planned reset. Expect roughly thirty minutes of downtime.</p>
</div>
<div style="font-family:'Inter',sans-serif;font-size:22px;color:#6b6151;"></div>
</a>
</div>
</div>
</sc-if>
<!-- ================= DEV BLOG ================= -->
<sc-if value="{{ isBlog }}" hint-placeholder-val="{{ false }}">
<div style="animation:fadeUp .5s ease both;max-width:880px;margin:0 auto;padding:72px 32px 100px;">
<div style="text-align:center;margin-bottom:60px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.3em;text-transform:uppercase;color:#c9a86a;margin-bottom:14px;">From the Workshop</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:54px;color:#f1e8d6;margin:0;letter-spacing:0.03em;">Developer Blog</h1>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:18px;color:#a99d8c;margin:18px auto 0;max-width:540px;">Behind-the-scenes development updates — the craft, the decisions, and the long work of building a living city.</p>
</div>
<div style="position:relative;padding-left:38px;">
<div style="position:absolute;left:8px;top:8px;bottom:8px;width:1px;background:linear-gradient(180deg,rgba(201,168,106,0.5),rgba(201,168,106,0.08));"></div>
<article style="position:relative;margin-bottom:48px;">
<div style="position:absolute;left:-34px;top:6px;width:13px;height:13px;border:1.5px solid #c9a86a;background:#100b18;transform:rotate(45deg);"></div>
<div style="background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.16);border-radius:6px;overflow:hidden;">
<div style="height:200px;background:repeating-linear-gradient(135deg,#221931,#221931 13px,#1b1428 13px,#1b1428 26px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(201,168,106,0.12);"><span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;color:#5e5446;text-transform:uppercase;">screenshot / wip render</span></div>
<div style="padding:34px 38px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
<span style="font-family:'Inter',sans-serif;font-size:10.5px;letter-spacing:0.1em;text-transform:uppercase;color:#9f86b8;border:1px solid rgba(159,134,184,0.4);padding:3px 10px;border-radius:2px;">Worldbuilding</span>
<span style="font-family:'Inter',sans-serif;font-size:12px;color:#8a7e6d;">22 June 2026 · 7 min read</span>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:27px;line-height:1.25;color:#f1e8d6;margin:0 0 16px;">Building the Undergate: Tunnels Beneath the City</h2>
<p style="font-family:'Jost',sans-serif;font-size:16px;line-height:1.9;color:#a99d8c;margin:0 0 22px;">Westgate has always been a city of two halves — the lantern-lit streets above, and the flooded warrens below. This month we walk you through how the Undergate came together: the layout philosophy, the lighting passes that took far longer than we'd like to admit, and the factions that now call the dark home.</p>
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#2a2035,#1a1322);border:1px solid rgba(201,168,106,0.3);"></div>
<span style="font-family:'Inter',sans-serif;font-size:13px;color:#b3a796;">archvillainette</span>
<span style="color:#5e5446;">·</span>
<a href="https://westgate.pw/category/84/developer-blog" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.08em;text-transform:uppercase;color:#c9a86a;">Read Entry →</a>
</div>
</div>
</div>
</article>
<article style="position:relative;margin-bottom:48px;">
<div style="position:absolute;left:-34px;top:6px;width:13px;height:13px;border:1.5px solid #c9a86a;background:#100b18;transform:rotate(45deg);"></div>
<div style="background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.16);border-radius:6px;padding:30px 36px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
<span style="font-family:'Inter',sans-serif;font-size:10.5px;letter-spacing:0.1em;text-transform:uppercase;color:#c9a86a;border:1px solid rgba(201,168,106,0.4);padding:3px 10px;border-radius:2px;">Systems</span>
<span style="font-family:'Inter',sans-serif;font-size:12px;color:#8a7e6d;">9 June 2026 · 6 min read</span>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:25px;line-height:1.25;color:#f1e8d6;margin:0 0 14px;">Faction Systems & the Masked Guilds</h2>
<p style="font-family:'Jost',sans-serif;font-size:15.5px;line-height:1.85;color:#a99d8c;margin:0 0 20px;">Reputation, favour, and betrayal — how the guild standing system tracks the webs of loyalty players weave, and why we chose to let almost everything be undone with the right bribe.</p>
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#2a2035,#1a1322);border:1px solid rgba(201,168,106,0.3);"></div>
<span style="font-family:'Inter',sans-serif;font-size:13px;color:#b3a796;">The Westgate Team</span>
<span style="color:#5e5446;">·</span>
<a href="https://westgate.pw/category/84/developer-blog" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.08em;text-transform:uppercase;color:#c9a86a;">Read Entry →</a>
</div>
</div>
</article>
<article style="position:relative;margin-bottom:48px;">
<div style="position:absolute;left:-34px;top:6px;width:13px;height:13px;border:1.5px solid #c9a86a;background:#100b18;transform:rotate(45deg);"></div>
<div style="background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.16);border-radius:6px;padding:30px 36px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
<span style="font-family:'Inter',sans-serif;font-size:10.5px;letter-spacing:0.1em;text-transform:uppercase;color:#9f86b8;border:1px solid rgba(159,134,184,0.4);padding:3px 10px;border-radius:2px;">Art & Atmosphere</span>
<span style="font-family:'Inter',sans-serif;font-size:12px;color:#8a7e6d;">27 May 2026 · 5 min read</span>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:25px;line-height:1.25;color:#f1e8d6;margin:0 0 14px;">Lighting Westgate: A Study in Lanternlight</h2>
<p style="font-family:'Jost',sans-serif;font-size:15.5px;line-height:1.85;color:#a99d8c;margin:0 0 20px;">Mood is everything in a city built on shadow. A look at our approach to colour temperature, fog, and the hundreds of placed light sources that make the docks feel cold and the taverns feel like refuge.</p>
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#2a2035,#1a1322);border:1px solid rgba(201,168,106,0.3);"></div>
<span style="font-family:'Inter',sans-serif;font-size:13px;color:#b3a796;">archvillainette</span>
<span style="color:#5e5446;">·</span>
<a href="https://westgate.pw/category/84/developer-blog" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.08em;text-transform:uppercase;color:#c9a86a;">Read Entry →</a>
</div>
</div>
</article>
<article style="position:relative;">
<div style="position:absolute;left:-34px;top:6px;width:13px;height:13px;border:1.5px solid #c9a86a;background:#100b18;transform:rotate(45deg);"></div>
<div style="background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.16);border-radius:6px;padding:30px 36px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
<span style="font-family:'Inter',sans-serif;font-size:10.5px;letter-spacing:0.1em;text-transform:uppercase;color:#c9a86a;border:1px solid rgba(201,168,106,0.4);padding:3px 10px;border-radius:2px;">Design</span>
<span style="font-family:'Inter',sans-serif;font-size:12px;color:#8a7e6d;">14 May 2026 · 8 min read</span>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:25px;line-height:1.25;color:#f1e8d6;margin:0 0 14px;">Why We Chose Persistent Roleplay</h2>
<p style="font-family:'Jost',sans-serif;font-size:15.5px;line-height:1.85;color:#a99d8c;margin:0 0 20px;">The first entry. Who we are, what Shadows Over Westgate is trying to be, and the kind of stories we hope you'll tell in its streets.</p>
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#2a2035,#1a1322);border:1px solid rgba(201,168,106,0.3);"></div>
<span style="font-family:'Inter',sans-serif;font-size:13px;color:#b3a796;">The Westgate Team</span>
<span style="color:#5e5446;">·</span>
<a href="https://westgate.pw/category/84/developer-blog" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.08em;text-transform:uppercase;color:#c9a86a;">Read Entry →</a>
</div>
</div>
</article>
</div>
</div>
</sc-if>
<!-- ================= GALLERY ================= -->
<sc-if value="{{ isGallery }}" hint-placeholder-val="{{ false }}">
<div style="animation:fadeUp .5s ease both;max-width:1240px;margin:0 auto;padding:72px 32px 100px;">
<div style="text-align:center;margin-bottom:56px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.3em;text-transform:uppercase;color:#c9a86a;margin-bottom:14px;">The Archive</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:54px;color:#f1e8d6;margin:0;letter-spacing:0.03em;">Gallery</h1>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:18px;color:#a99d8c;margin:18px auto 0;max-width:540px;">Scenes from the streets of Westgate, captured by its denizens. Drag your own screenshots onto any frame.</p>
</div>
<div style="column-count:3;column-gap:18px;">
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g1" shape="rect" placeholder="Drop screenshot" style="width:100%;height:300px;display:block;" hint-size="100%,300px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">The Undergate at Low Tide</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@duskwarden</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g2" shape="rect" placeholder="Drop screenshot" style="width:100%;height:220px;display:block;" hint-size="100%,220px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">Rain over the Harbour</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@archvillainette</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g3" shape="rect" placeholder="Drop screenshot" style="width:100%;height:340px;display:block;" hint-size="100%,340px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">Noble Row, House Malforten</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@velvethand</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g4" shape="rect" placeholder="Drop screenshot" style="width:100%;height:240px;display:block;" hint-size="100%,240px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">Masks of the Quivering Thumb</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@silktongue</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g5" shape="rect" placeholder="Drop screenshot" style="width:100%;height:300px;display:block;" hint-size="100%,300px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">The Tankard's Rest</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@ghostofeltabbar</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g6" shape="rect" placeholder="Drop screenshot" style="width:100%;height:230px;display:block;" hint-size="100%,230px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">Cobbles after Curfew</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@ashen</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g7" shape="rect" placeholder="Drop screenshot" style="width:100%;height:330px;display:block;" hint-size="100%,330px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">The Sea of Fallen Stars at Dusk</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@cartographer</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g8" shape="rect" placeholder="Drop screenshot" style="width:100%;height:250px;display:block;" hint-size="100%,250px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">A Bargain Beneath the Lantern</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@nightjar</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g9" shape="rect" placeholder="Drop screenshot" style="width:100%;height:290px;display:block;" hint-size="100%,290px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">Something in the Undergate</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@nameless</div></div>
</div>
</div>
</div>
</sc-if>
<!-- ================= JOIN THE TEAM ================= -->
<sc-if value="{{ isRecruit }}" hint-placeholder-val="{{ false }}">
<div style="animation:fadeUp .5s ease both;">
<!-- HERO -->
<section style="max-width:1240px;margin:0 auto;padding:82px 32px 20px;">
<div style="display:grid;grid-template-columns:1.05fr 1fr;gap:56px;align-items:center;">
<div>
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c9a86a;margin-bottom:22px;">Persistent World · Made by Hand</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:52px;line-height:1.08;letter-spacing:0.02em;color:#f1e8d6;margin:0 0 22px;">Come build a world that's a pleasure to work on.</h1>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:18px;line-height:1.7;color:#bcb09e;margin:0 0 34px;max-width:30em;">Westgate is made by a handful of people who love this stuff — artists, writers, builders, scripters. We've kept the tools pleasant so your time goes where it matters: into the world itself. If you make things, we'd love to meet you.</p>
<div style="display:flex;align-items:center;gap:22px;flex-wrap:wrap;">
<a href="https://westgate.pw/categories" style="font-family:'Inter',sans-serif;font-size:13px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:15px 30px;border-radius:3px;border:1px solid #ddc488;box-shadow:0 4px 22px rgba(168,133,74,0.32);">Say Hello on Discord</a>
</div>
</div>
<div style="background:linear-gradient(160deg,#1a1322,#100b18);border:1px solid rgba(201,168,106,0.2);border-radius:6px;overflow:hidden;box-shadow:0 24px 60px rgba(0,0,0,0.5);">
<div style="display:flex;align-items:center;gap:9px;padding:13px 16px;border-bottom:1px solid rgba(201,168,106,0.16);">
<span style="display:flex;gap:6px;"><i style="width:9px;height:9px;border-radius:50%;background:#7fc07f;display:block;"></i><i style="width:9px;height:9px;border-radius:50%;background:#d3b573;display:block;"></i><i style="width:9px;height:9px;border-radius:50%;background:#9f86b8;display:block;"></i></span>
<span style="font-family:'Inter',monospace;font-size:12px;color:#8a7e6d;margin-left:6px;">westgate — the undergate</span>
</div>
<div style="aspect-ratio:16/11;background:repeating-linear-gradient(135deg,#1c1526,#1c1526 11px,#181120 11px,#181120 22px);display:flex;align-items:center;justify-content:center;">
<span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:#5e5446;background:#100b18;border:1px solid rgba(201,168,106,0.16);padding:7px 13px;border-radius:3px;">screenshot · a corner you'll help build</span>
</div>
</div>
</div>
</section>
<!-- PILLARS -->
<section id="wg-stack" style="max-width:1240px;margin:0 auto;padding:80px 32px 30px;scroll-margin-top:80px;">
<div style="text-align:center;margin-bottom:46px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c9a86a;margin-bottom:12px;">Who We Need</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:36px;color:#ece2d2;margin:0 0 14px;letter-spacing:0.03em;">Every kind of maker has a place here</h2>
<p style="font-family:'Jost',sans-serif;font-size:16px;line-height:1.7;color:#a99d8c;margin:0 auto;max-width:32em;">Code, art, words, world — whatever you make, there's a part of Westgate waiting for it.</p>
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:22px;">
<article style="background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;overflow:hidden;display:flex;flex-direction:column;">
<div style="aspect-ratio:16/9;background:repeating-linear-gradient(135deg,#1c1526,#1c1526 11px,#181120 11px,#181120 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(201,168,106,0.12);"><span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:#5e5446;">toolset · scripts</span></div>
<div style="padding:26px 26px 28px;">
<h3 style="font-family:'Cinzel',serif;font-size:20px;color:#e8dcc6;margin:0 0 10px;">Tools that stay out of your way</h3>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#998d7c;margin:0;">For builders and scripters: a clean toolset and modern scripting on a fast core — none of NWScript's old quirks to fight. Everything's saved and versioned, so you can experiment without fear of breaking things.</p>
</div>
</article>
<article style="background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;overflow:hidden;display:flex;flex-direction:column;">
<div style="aspect-ratio:16/9;background:repeating-linear-gradient(135deg,#1c1526,#1c1526 11px,#181120 11px,#181120 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(201,168,106,0.12);"><span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:#5e5446;">models · textures · music</span></div>
<div style="padding:26px 26px 28px;">
<h3 style="font-family:'Cinzel',serif;font-size:20px;color:#e8dcc6;margin:0 0 10px;">Your craft, your stamp</h3>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#998d7c;margin:0;">For artists and asset-makers: models, textures, areas, sound. We're not after a house style to copy — we want the thing only you'd make, and we'll help get it into the game.</p>
</div>
</article>
<article style="background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;overflow:hidden;display:flex;flex-direction:column;">
<div style="aspect-ratio:16/9;background:repeating-linear-gradient(135deg,#1c1526,#1c1526 11px,#181120 11px,#181120 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(201,168,106,0.12);"><span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:#5e5446;">lore · quests · factions</span></div>
<div style="padding:26px 26px 28px;">
<h3 style="font-family:'Cinzel',serif;font-size:20px;color:#e8dcc6;margin:0 0 10px;">Ideas that reach players</h3>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#998d7c;margin:0;">For writers and designers: a questline, a faction, a forgotten corner of the city. What you dream up this month is something players are living in the next.</p>
</div>
</article>
</div>
</section>
<!-- TEAM -->
<section style="max-width:1240px;margin:0 auto;padding:56px 32px 30px;">
<div style="background:linear-gradient(160deg,#1a1322,#100b18);border:1px solid rgba(201,168,106,0.2);border-radius:6px;box-shadow:0 20px 50px rgba(0,0,0,0.4);padding:46px 50px;display:grid;grid-template-columns:1.1fr 1fr;gap:50px;align-items:center;">
<div>
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c9a86a;margin-bottom:14px;">What It's Actually Like</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:30px;line-height:1.2;color:#f1e8d6;margin:0 0 16px;">Low ego, high trust — and your work goes live.</h2>
<p style="font-family:'Jost',sans-serif;font-size:16px;line-height:1.85;color:#a99d8c;margin:0;">No gatekeeping, no hazing, no “figure it out yourself.” We give honest, kind feedback, lend a hand when it helps, and keep things moving. The thing you make this week is the thing players see this week.</p>
</div>
<div style="display:flex;flex-direction:column;gap:16px;">
<div style="display:flex;align-items:center;gap:14px;font-family:'Jost',sans-serif;font-size:15.5px;color:#cabfae;"><span style="width:7px;height:7px;background:#c9a86a;transform:rotate(45deg);flex:none;"></span>Honest, kind feedback on your work</div>
<div style="display:flex;align-items:center;gap:14px;font-family:'Jost',sans-serif;font-size:15.5px;color:#cabfae;"><span style="width:7px;height:7px;background:#c9a86a;transform:rotate(45deg);flex:none;"></span>A hand whenever you want one</div>
<div style="display:flex;align-items:center;gap:14px;font-family:'Jost',sans-serif;font-size:15.5px;color:#cabfae;"><span style="width:7px;height:7px;background:#c9a86a;transform:rotate(45deg);flex:none;"></span>What you make can be in-game within the week</div>
<div style="display:flex;align-items:center;gap:14px;font-family:'Jost',sans-serif;font-size:15.5px;color:#cabfae;"><span style="width:7px;height:7px;background:#c9a86a;transform:rotate(45deg);flex:none;"></span>Work at whatever pace fits your life</div>
</div>
</div>
</section>
<!-- CTA -->
<section style="max-width:760px;margin:0 auto;padding:70px 32px 100px;text-align:center;">
<div style="display:flex;align-items:center;justify-content:center;gap:18px;margin-bottom:40px;">
<div style="height:1px;width:80px;background:linear-gradient(90deg,transparent,#c9a86a);"></div>
<div style="width:7px;height:7px;background:#c9a86a;transform:rotate(45deg);"></div>
<div style="height:1px;width:80px;background:linear-gradient(90deg,#c9a86a,transparent);"></div>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:700;font-size:40px;color:#f1e8d6;margin:0 0 16px;letter-spacing:0.02em;">Sound like your kind of place?</h2>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:18px;line-height:1.7;color:#bcb09e;margin:0 0 30px;">No test gauntlet, no résumé. Just come say hi and tell us what you'd love to build.</p>
<!-- ponytail: point at the real Discord invite when you have it -->
<a href="https://westgate.pw/categories" style="display:inline-block;font-family:'Inter',sans-serif;font-size:13px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:16px 36px;border-radius:3px;border:1px solid #ddc488;box-shadow:0 4px 26px rgba(168,133,74,0.4);">Join the Team on Discord →</a>
<div style="margin-top:46px;padding-top:22px;border-top:1px solid rgba(201,168,106,0.12);font-family:'Inter',sans-serif;font-size:12px;letter-spacing:0.08em;color:#6d6356;">Artists · writers · builders · scripters — all welcome</div>
</section>
</div>
</sc-if>
<!-- ================= FOOTER ================= -->
<footer style="border-top:1px solid rgba(201,168,106,0.16);background:linear-gradient(180deg,rgba(13,9,18,0),rgba(8,5,12,0.6));margin-top:20px;">
<div style="max-width:1240px;margin:0 auto;padding:64px 32px 30px;display:grid;grid-template-columns:1.6fr 1fr 1fr;gap:50px;">
<div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:18px;">
<div style="width:22px;height:22px;transform:rotate(45deg);border:1.5px solid #c9a86a;display:flex;align-items:center;justify-content:center;"><div style="width:7px;height:7px;background:#c9a86a;"></div></div>
<div style="font-family:'Cinzel',serif;font-weight:600;font-size:15px;letter-spacing:0.14em;color:#e8dcc6;">SHADOWS OVER WESTGATE</div>
</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.8;color:#8a7e6d;margin:0 0 18px;max-width:380px;">A Neverwinter Nights: Enhanced Edition persistent world of gothic intrigue, set in the Forgotten Realms port city of Westgate.</p>
<p style="font-family:'Cinzel',serif;font-style:italic;font-size:14px;color:#9a8262;margin:0;">Mind the shadows.</p>
</div>
<div>
<div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.16em;text-transform:uppercase;color:#c9a86a;margin-bottom:18px;">Explore</div>
<div style="display:flex;flex-direction:column;gap:12px;">
<div onClick="{{ goHome }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">Home</div>
<div onClick="{{ goNews }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">News</div>
<div onClick="{{ goGallery }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">Gallery</div>
<div onClick="{{ goBlog }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">Dev Blog</div>
<div onClick="{{ goRecruit }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">Join the Team</div>
</div>
</div>
<div>
<div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.16em;text-transform:uppercase;color:#c9a86a;margin-bottom:18px;">Community</div>
<div style="display:flex;flex-direction:column;gap:12px;">
<a href="https://westgate.pw/categories" style="font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">Forums</a>
<a href="https://westgate.pw/wiki" style="font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">Wiki</a>
<a href="https://westgate.pw/register" style="font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">Register</a>
<a href="https://westgate.pw/wiki" style="font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">How to Join</a>
</div>
</div>
</div>
<div style="max-width:1240px;margin:0 auto;padding:22px 32px 40px;border-top:1px solid rgba(201,168,106,0.1);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;">
<div style="font-family:'Inter',sans-serif;font-size:12px;color:#6d6356;">© 2026 Shadows Over Westgate · A NWN:EE Persistent World</div>
<div style="font-family:'Inter',sans-serif;font-size:12px;color:#6d6356;">Forgotten Realms · Westgate · Sea of Fallen Stars</div>
</div>
</footer>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:1280,&quot;height&quot;:860},&quot;startPage&quot;:{&quot;editor&quot;:&quot;enum&quot;,&quot;options&quot;:[&quot;home&quot;,&quot;news&quot;,&quot;blog&quot;,&quot;gallery&quot;,&quot;recruit&quot;],&quot;default&quot;:&quot;home&quot;,&quot;tsType&quot;:&quot;'home'|'news'|'blog'|'gallery'|'recruit'&quot;}}">
class Component extends DCLogic {
state = { page: this.props.startPage || 'home' };
go(page) {
this.setState({ page });
if (typeof window !== 'undefined') {
try { window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (e) { window.scrollTo(0, 0); }
}
}
renderVals() {
const p = this.state.page;
return {
isHome: p === 'home',
isNews: p === 'news',
isBlog: p === 'blog',
isGallery: p === 'gallery',
isRecruit: p === 'recruit',
goHome: () => this.go('home'),
goNews: () => this.go('news'),
goBlog: () => this.go('blog'),
goGallery: () => this.go('gallery'),
goRecruit: () => this.go('recruit'),
};
}
}
</script>
</body>
</html>
+641
View File
@@ -0,0 +1,641 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=Jost:wght@300;400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;}
body{margin:0;background:#09080b;}
::selection{background:rgba(194,163,90,0.28);color:#f3ead9;}
@keyframes fadeUp{from{transform:translateY(14px);}to{transform:translateY(0);}}
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(127,184,106,0.5);}70%{box-shadow:0 0 0 9px rgba(127,184,106,0);}100%{box-shadow:0 0 0 0 rgba(127,184,106,0);}}
@keyframes drift{from{background-position:0 0;}to{background-position:60px 60px;}}
a{text-decoration:none;color:inherit;}
.wg-scroll::-webkit-scrollbar{width:0;height:0;}
</style>
</helmet>
<div style="min-height:100vh;font-family:'Jost',sans-serif;color:#b9b2a6;background:radial-gradient(120% 70% at 50% -10%, #1a1020 0%, #120e16 40%, #0f0d12 72%);background-attachment:fixed;overflow-x:hidden;">
<!-- ================= NAV ================= -->
<nav style="position:sticky;top:0;z-index:50;backdrop-filter:blur(14px);background:rgba(15,13,18,0.84);border-bottom:1px solid rgba(194,163,90,0.16);">
<div style="max-width:1240px;margin:0 auto;padding:0 32px;height:66px;display:flex;align-items:center;justify-content:space-between;gap:24px;">
<div onClick="{{ goHome }}" style="display:flex;align-items:center;gap:13px;cursor:pointer;">
<div style="width:26px;height:26px;transform:rotate(45deg);border:1.5px solid #c2a35a;display:flex;align-items:center;justify-content:center;box-shadow:0 0 14px rgba(194,163,90,0.25);">
<div style="width:9px;height:9px;background:#c2a35a;"></div>
</div>
<div style="font-family:'Cinzel',serif;font-weight:600;font-size:16px;letter-spacing:0.16em;color:#e6e0d6;white-space:nowrap;">SHADOWS OVER WESTGATE</div>
</div>
<div style="display:flex;align-items:center;gap:30px;">
<div onClick="{{ goHome }}" style="position:relative;cursor:pointer;font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;" style-hover="color:#e6e0d6;">Home
<sc-if value="{{ isHome }}" hint-placeholder-val="{{ true }}"><span style="position:absolute;left:-4px;right:-4px;bottom:-21px;height:2px;background:linear-gradient(90deg,transparent,#c2a35a,transparent);"></span></sc-if>
</div>
<div onClick="{{ goNews }}" style="position:relative;cursor:pointer;font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;" style-hover="color:#e6e0d6;">News
<sc-if value="{{ isNews }}" hint-placeholder-val="{{ false }}"><span style="position:absolute;left:-4px;right:-4px;bottom:-21px;height:2px;background:linear-gradient(90deg,transparent,#c2a35a,transparent);"></span></sc-if>
</div>
<a href="https://westgate.pw/categories" style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;" style-hover="color:#e6e0d6;">Forums</a>
<a href="https://westgate.pw/wiki" style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;" style-hover="color:#e6e0d6;">Wiki</a>
<div onClick="{{ goGallery }}" style="position:relative;cursor:pointer;font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;" style-hover="color:#e6e0d6;">Gallery
<sc-if value="{{ isGallery }}" hint-placeholder-val="{{ false }}"><span style="position:absolute;left:-4px;right:-4px;bottom:-21px;height:2px;background:linear-gradient(90deg,transparent,#c2a35a,transparent);"></span></sc-if>
</div>
<div onClick="{{ goBlog }}" style="position:relative;cursor:pointer;font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;white-space:nowrap;" style-hover="color:#e6e0d6;">Dev Blog
<sc-if value="{{ isBlog }}" hint-placeholder-val="{{ false }}"><span style="position:absolute;left:-4px;right:-4px;bottom:-21px;height:2px;background:linear-gradient(90deg,transparent,#c2a35a,transparent);"></span></sc-if>
</div>
</div>
<div style="display:flex;align-items:center;gap:14px;">
<a href="https://westgate.pw/login" style="font-family:'Jost',sans-serif;font-size:12px;letter-spacing:0.1em;text-transform:uppercase;color:#c4b8a6;white-space:nowrap;" style-hover="color:#e6e0d6;">Log In</a>
<a href="https://westgate.pw/register" style="font-family:'Jost',sans-serif;font-size:12px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:9px 18px;border-radius:3px;border:1px solid #ddc488;box-shadow:0 2px 12px rgba(168,133,74,0.3);">Register</a>
</div>
</div>
</nav>
<!-- ================= HOME ================= -->
<sc-if value="{{ isHome }}" hint-placeholder-val="{{ true }}">
<div style="animation:fadeUp .5s ease both;">
<!-- HERO -->
<section style="position:relative;height:560px;border-bottom:1px solid rgba(194,163,90,0.16);overflow:hidden;">
<div style="position:absolute;inset:0;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-hero" shape="rect" placeholder="Drop banner key art" style="width:100%;height:100%;" hint-size="100%,100%"></x-import>
</div>
<div style="position:absolute;inset:0;pointer-events:none;background:linear-gradient(180deg,rgba(10,7,14,0.55) 0%,rgba(10,7,14,0.2) 35%,rgba(10,7,14,0.78) 82%,#0a0710 100%);"></div>
<div style="position:absolute;inset:0;pointer-events:none;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:0 24px;">
<div style="font-family:'Cinzel',serif;font-size:13px;letter-spacing:0.42em;text-transform:uppercase;color:#c2a35a;margin-bottom:22px;">Neverwinter Nights · Persistent World</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:72px;line-height:1.02;letter-spacing:0.03em;color:#f3ede4;margin:0;text-shadow:0 4px 40px rgba(0,0,0,0.7);">Welcome to Westgate</h1>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:20px;color:#cfc5b7;max-width:620px;margin:24px 0 0;line-height:1.6;text-shadow:0 2px 16px rgba(0,0,0,0.8);">An ancient port city rises out of the mists, at the edge of the Sea of Fallen Stars. Mind the shadows.</p>
<div style="display:flex;gap:16px;margin-top:38px;pointer-events:auto;">
<a href="https://westgate.pw/wiki" style="font-family:'Jost',sans-serif;font-size:13px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:15px 32px;border-radius:3px;border:1px solid #ddc488;box-shadow:0 4px 22px rgba(168,133,74,0.35);">How to Join</a>
<div onClick="{{ goNews }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:13px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#e6e0d6;background:rgba(20,14,26,0.55);backdrop-filter:blur(6px);padding:15px 32px;border-radius:3px;border:1px solid rgba(194,163,90,0.4);">Latest News</div>
</div>
</div>
</section>
<!-- SERVER STATUS -->
<section style="max-width:1240px;margin:0 auto;padding:0 32px;">
<div style="margin-top:-44px;position:relative;z-index:10;background:linear-gradient(100deg, rgba(58,24,48,0.5) 0%, rgba(31,25,39,0.97) 23%, rgba(21,18,27,1) 62%, rgba(15,14,20,0.99) 100%);border:1px solid rgba(194,163,90,0.2);border-radius:5px;box-shadow:0 24px 60px rgba(0,0,0,0.5);padding:26px 34px;display:flex;align-items:center;gap:40px;flex-wrap:wrap;">
<div style="display:flex;align-items:center;gap:16px;">
<div style="width:13px;height:13px;border-radius:50%;background:#7fb86a;animation:pulse 2.4s infinite;"></div>
<div>
<div style="font-family:'Cinzel',serif;font-size:18px;letter-spacing:0.06em;color:#e6e0d6;">Server Online</div>
<div style="font-family:'Jost',sans-serif;font-size:12px;letter-spacing:0.08em;color:#9a9086;margin-top:3px;">Alpha · Module 0.3</div>
</div>
</div>
<div style="width:1px;height:42px;background:rgba(194,163,90,0.18);"></div>
<div>
<div style="font-family:'Cinzel',serif;font-size:26px;color:#e6e0d6;">14 <span style="font-size:15px;color:#9a9086;">souls online</span></div>
<div style="font-family:'Jost',sans-serif;font-size:12px;letter-spacing:0.08em;color:#9a9086;margin-top:2px;">Peaked 32 today</div>
</div>
<div style="width:1px;height:42px;background:rgba(194,163,90,0.18);"></div>
<div style="flex:1;min-width:180px;">
<div style="font-family:'Jost',sans-serif;font-size:11px;letter-spacing:0.16em;text-transform:uppercase;color:#c2a35a;">Direct Connect</div>
<div style="font-family:'Jost',sans-serif;font-size:15px;color:#cfc5b7;margin-top:4px;">play.westgate.pw</div>
</div>
<a href="https://westgate.pw/wiki" style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:13px 26px;border-radius:3px;border:1px solid #ddc488;">Connect Guide</a>
</div>
</section>
<!-- PITCH -->
<section style="max-width:760px;margin:0 auto;padding:96px 32px 30px;text-align:center;">
<div style="display:flex;align-items:center;justify-content:center;gap:18px;margin-bottom:40px;">
<div style="height:1px;width:80px;background:linear-gradient(90deg,transparent,#c2a35a);"></div>
<div style="width:7px;height:7px;background:#c2a35a;transform:rotate(45deg);"></div>
<div style="height:1px;width:80px;background:linear-gradient(90deg,#c2a35a,transparent);"></div>
</div>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:23px;line-height:1.7;color:#cfc5b7;margin:0 0 34px;">Long ago, merchants from distant lands filled its harbors, and gold flowed as freely as wine — but time, as it does, has a way of curdling mortal splendor.</p>
<p style="font-family:'Jost',sans-serif;font-size:17px;line-height:1.95;color:#9a9086;margin:0 0 26px;">Today its bones are old stone and salt-worn docks, and bargains are struck beneath every lantern's glow. Here, hoarded secrets, owed favour, and unspoken truths are the true measure of power — although coin certainly helps.</p>
<p style="font-family:'Jost',sans-serif;font-size:17px;line-height:1.95;color:#9a9086;margin:0 0 26px;">Westgate is a city ever-balanced on a blade's edge, built upon decaying decadence. The masked and nameless guilds clot the veins of the city with their greed and cruelty. Noble houses cling to fading legacies as their bloodlines tangle with curses, ambition, and desperation. Old cults and buried powers hold the Undergate by its throat. Beneath it all, antediluvian things begin to stir — patient, watchful, and hungry.</p>
<blockquote style="font-family:'Cinzel',serif;font-style:italic;font-size:22px;line-height:1.5;color:#d8c28a;margin:46px auto;max-width:560px;position:relative;">
<span style="position:absolute;left:50%;top:-34px;transform:translateX(-50%);font-size:54px;color:rgba(194,163,90,0.3);font-family:'Cinzel',serif;"></span>
To walk Westgate's streets is to wager your soul in a game of chance.
</blockquote>
<p style="font-family:'Jost',sans-serif;font-size:17px;line-height:1.95;color:#9a9086;margin:0 0 26px;">Every alley offers opportunity, yet each carries a price. While some come seeking fortune or power, others arrive hoping to disappear — and indeed they do, for in these boulevards you are lucky to merely survive. In Westgate, few leave unchanged… and fewer still leave at all.</p>
<p style="font-family:'Cinzel',serif;font-size:19px;letter-spacing:0.04em;color:#e6e0d6;margin:34px 0 0;">Welcome to Westgate. Mind the shadows.</p>
</section>
<!-- GET STARTED -->
<section style="max-width:1240px;margin:0 auto;padding:70px 32px 30px;">
<div style="text-align:center;margin-bottom:46px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c2a35a;margin-bottom:12px;">Your First Steps</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:38px;color:#e6e0d6;margin:0;letter-spacing:0.03em;">Enter the City</h2>
</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:20px;">
<a href="https://westgate.pw/wiki" style="display:block;background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.14);border-radius:5px;padding:30px 26px;transition:all .25s;" style-hover="border:1px solid rgba(194,163,90,0.4);transform:translateY(-4px);">
<div style="width:42px;height:42px;border:1px solid rgba(194,163,90,0.4);border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:22px;"><div style="width:13px;height:13px;border:1.5px solid #c2a35a;transform:rotate(45deg);"></div></div>
<div style="font-family:'Cinzel',serif;font-size:19px;color:#e6e0d6;margin-bottom:10px;">How to Join</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#9a9086;margin:0;">Install Neverwinter Nights: EE, point it at our server, and step through the gate.</p>
</a>
<a href="https://westgate.pw/wiki" style="display:block;background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.14);border-radius:5px;padding:30px 26px;transition:all .25s;" style-hover="border:1px solid rgba(194,163,90,0.4);transform:translateY(-4px);">
<div style="width:42px;height:42px;border:1px solid rgba(194,163,90,0.4);border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:22px;"><div style="width:13px;height:13px;border:1.5px solid #c2a35a;"></div></div>
<div style="font-family:'Cinzel',serif;font-size:19px;color:#e6e0d6;margin-bottom:10px;">New Player Wiki</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#9a9086;margin:0;">Starter guides, character creation, and everything you need for your first night.</p>
</a>
<a href="https://westgate.pw/categories" style="display:block;background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.14);border-radius:5px;padding:30px 26px;transition:all .25s;" style-hover="border:1px solid rgba(194,163,90,0.4);transform:translateY(-4px);">
<div style="width:42px;height:42px;border:1px solid rgba(194,163,90,0.4);border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:22px;"><div style="width:15px;height:11px;border:1.5px solid #c2a35a;border-radius:2px;"></div></div>
<div style="font-family:'Cinzel',serif;font-size:19px;color:#e6e0d6;margin-bottom:10px;">The Setting Bible</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#9a9086;margin:0;">Lore of Westgate — its districts, guilds, noble houses, and buried histories.</p>
</a>
<a href="https://westgate.pw/categories" style="display:block;background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.14);border-radius:5px;padding:30px 26px;transition:all .25s;" style-hover="border:1px solid rgba(194,163,90,0.4);transform:translateY(-4px);">
<div style="width:42px;height:42px;border:1px solid rgba(194,163,90,0.4);border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:22px;"><div style="width:11px;height:11px;border-radius:50%;border:1.5px solid #c2a35a;"></div></div>
<div style="font-family:'Cinzel',serif;font-size:19px;color:#e6e0d6;margin-bottom:10px;">Join the Community</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#9a9086;margin:0;">Introduce yourself in the forums and find your place among Westgate's denizens.</p>
</a>
</div>
</section>
<!-- LATEST NEWS STRIP -->
<section style="max-width:1240px;margin:0 auto;padding:70px 32px 100px;">
<div style="display:flex;align-items:flex-end;justify-content:space-between;margin-bottom:34px;">
<div>
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c2a35a;margin-bottom:10px;">From the Watch</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:34px;color:#e6e0d6;margin:0;letter-spacing:0.03em;">Latest News</h2>
</div>
<div onClick="{{ goNews }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:12px;letter-spacing:0.12em;text-transform:uppercase;color:#c2a35a;border-bottom:1px solid rgba(194,163,90,0.4);padding-bottom:3px;">All Dispatches →</div>
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:22px;">
<div onClick="{{ goNews }}" style="cursor:pointer;background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.14);border-radius:5px;overflow:hidden;transition:all .25s;" style-hover="border:1px solid rgba(194,163,90,0.4);">
<div style="height:160px;background:repeating-linear-gradient(135deg,#1b1320,#1b1320 11px,#161019 11px,#161019 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(194,163,90,0.12);"><span style="font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:0.15em;color:#6b6152;text-transform:uppercase;">news image</span></div>
<div style="padding:24px;">
<div style="font-family:'Jost',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#9a9086;margin-bottom:10px;">24 Jun 2026 · Announcement</div>
<div style="font-family:'Cinzel',serif;font-size:19px;line-height:1.35;color:#e6e0d6;margin-bottom:10px;">The Gates Open — Alpha Has Begun</div>
<p style="font-family:'Jost',sans-serif;font-size:14px;line-height:1.7;color:#9a9086;margin:0;">Westgate opens its harbour to its first wave of adventurers. Here is everything you need to know.</p>
</div>
</div>
<div onClick="{{ goNews }}" style="cursor:pointer;background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.14);border-radius:5px;overflow:hidden;transition:all .25s;" style-hover="border:1px solid rgba(194,163,90,0.4);">
<div style="height:160px;background:repeating-linear-gradient(135deg,#1b1320,#1b1320 11px,#161019 11px,#161019 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(194,163,90,0.12);"><span style="font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:0.15em;color:#6b6152;text-transform:uppercase;">news image</span></div>
<div style="padding:24px;">
<div style="font-family:'Jost',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#9a9086;margin-bottom:10px;">21 Jun 2026 · Patch Note</div>
<div style="font-family:'Cinzel',serif;font-size:19px;line-height:1.35;color:#e6e0d6;margin-bottom:10px;">Patch 0.3 — The Undergate</div>
<p style="font-family:'Jost',sans-serif;font-size:14px;line-height:1.7;color:#9a9086;margin:0;">The tunnels beneath the city are now open. New areas, factions, and dangers await below.</p>
</div>
</div>
<div onClick="{{ goNews }}" style="cursor:pointer;background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.14);border-radius:5px;overflow:hidden;transition:all .25s;" style-hover="border:1px solid rgba(194,163,90,0.4);">
<div style="height:160px;background:repeating-linear-gradient(135deg,#1b1320,#1b1320 11px,#161019 11px,#161019 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(194,163,90,0.12);"><span style="font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:0.15em;color:#6b6152;text-transform:uppercase;">news image</span></div>
<div style="padding:24px;">
<div style="font-family:'Jost',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#9a9086;margin-bottom:10px;">18 Jun 2026 · Community</div>
<div style="font-family:'Cinzel',serif;font-size:19px;line-height:1.35;color:#e6e0d6;margin-bottom:10px;">The Community Charter</div>
<p style="font-family:'Jost',sans-serif;font-size:14px;line-height:1.7;color:#9a9086;margin:0;">Our revised rules and the principles that keep Westgate's streets worth walking.</p>
</div>
</div>
</div>
</section>
</div>
</sc-if>
<!-- ================= NEWS ================= -->
<sc-if value="{{ isNews }}" hint-placeholder-val="{{ false }}">
<div style="animation:fadeUp .5s ease both;max-width:1100px;margin:0 auto;padding:72px 32px 100px;">
<div style="text-align:center;margin-bottom:56px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.3em;text-transform:uppercase;color:#c2a35a;margin-bottom:14px;">Dispatches from the Watch</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:54px;color:#f3ede4;margin:0;letter-spacing:0.03em;">News</h1>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:18px;color:#9a9086;margin:18px auto 0;max-width:520px;">Official server announcements, patch notes, and word from those who keep the gates.</p>
</div>
<!-- featured -->
<div style="background:linear-gradient(100deg, rgba(58,24,48,0.5) 0%, rgba(31,25,39,0.97) 23%, rgba(21,18,27,1) 62%, rgba(15,14,20,0.99) 100%);border:1px solid rgba(194,163,90,0.2);border-radius:6px;overflow:hidden;display:grid;grid-template-columns:1.15fr 1fr;margin-bottom:46px;box-shadow:0 20px 50px rgba(0,0,0,0.4);">
<div style="position:relative;min-height:320px;background:repeating-linear-gradient(135deg,#241a30,#241a30 13px,#1a1326 13px,#1a1326 26px);display:flex;align-items:center;justify-content:center;">
<span style="font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:0.15em;color:#6b6152;text-transform:uppercase;">feature image</span>
</div>
<div style="padding:46px 44px;display:flex;flex-direction:column;justify-content:center;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:18px;">
<span style="font-family:'Jost',sans-serif;font-size:10.5px;letter-spacing:0.12em;text-transform:uppercase;color:#150f08;background:#c2a35a;padding:4px 11px;border-radius:2px;font-weight:600;">Announcement</span>
<span style="font-family:'Jost',sans-serif;font-size:12px;letter-spacing:0.06em;color:#9a9086;">24 June 2026</span>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:31px;line-height:1.2;color:#f3ede4;margin:0 0 18px;">The Gates of Westgate Open — Alpha Has Begun</h2>
<p style="font-family:'Jost',sans-serif;font-size:16px;line-height:1.85;color:#9a9086;margin:0 0 26px;">After long months in the shipyards, the harbour is open. Our alpha welcomes its first denizens to walk the salt-worn docks, strike bargains beneath the lanterns, and write the first chapters of Westgate's living history. Read on for what to expect, how to join, and where to begin.</p>
<div style="display:flex;align-items:center;gap:14px;">
<div onClick="{{ goArticle }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:12px 26px;border-radius:3px;border:1px solid #ddc488;">Read Dispatch</div>
<span style="font-family:'Jost',sans-serif;font-size:12px;color:#9a9086;">by The Watch · 4 min read</span>
</div>
</div>
</div>
<!-- list -->
<div style="display:flex;flex-direction:column;">
<a href="https://westgate.pw/category/1/news" style="display:grid;grid-template-columns:130px 1fr auto;gap:28px;align-items:center;padding:26px 8px;border-top:1px solid rgba(194,163,90,0.12);transition:background .2s;" style-hover="background:rgba(194,163,90,0.04);">
<div style="text-align:center;"><div style="font-family:'Cinzel',serif;font-size:30px;color:#d8c28a;line-height:1;">21</div><div style="font-family:'Jost',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#9a9086;margin-top:5px;">Jun 2026</div></div>
<div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:7px;"><span style="font-family:'Jost',sans-serif;font-size:10px;letter-spacing:0.12em;text-transform:uppercase;color:#bf8a86;border:1px solid rgba(166,74,78,0.4);padding:2px 8px;border-radius:2px;">Patch Note</span></div>
<div style="font-family:'Cinzel',serif;font-size:21px;color:#e6e0d6;margin-bottom:6px;">Patch 0.3 — The Undergate Expansion</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.6;color:#9a9086;margin:0;">New sub-levels, the Quivering Thumb questline, balance passes to thievery, and a reworked lantern lighting system.</p>
</div>
<div style="font-family:'Jost',sans-serif;font-size:22px;color:#6b6151;"></div>
</a>
<a href="https://westgate.pw/category/1/news" style="display:grid;grid-template-columns:130px 1fr auto;gap:28px;align-items:center;padding:26px 8px;border-top:1px solid rgba(194,163,90,0.12);transition:background .2s;" style-hover="background:rgba(194,163,90,0.04);">
<div style="text-align:center;"><div style="font-family:'Cinzel',serif;font-size:30px;color:#d8c28a;line-height:1;">18</div><div style="font-family:'Jost',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#9a9086;margin-top:5px;">Jun 2026</div></div>
<div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:7px;"><span style="font-family:'Jost',sans-serif;font-size:10px;letter-spacing:0.12em;text-transform:uppercase;color:#c2a35a;border:1px solid rgba(194,163,90,0.4);padding:2px 8px;border-radius:2px;">Community</span></div>
<div style="font-family:'Cinzel',serif;font-size:21px;color:#e6e0d6;margin-bottom:6px;">The Community Charter & Rules Revision</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.6;color:#9a9086;margin:0;">An update to our code of conduct and roleplay standards. Please review before your next session.</p>
</div>
<div style="font-family:'Jost',sans-serif;font-size:22px;color:#6b6151;"></div>
</a>
<a href="https://westgate.pw/category/1/news" style="display:grid;grid-template-columns:130px 1fr auto;gap:28px;align-items:center;padding:26px 8px;border-top:1px solid rgba(194,163,90,0.12);transition:background .2s;" style-hover="background:rgba(194,163,90,0.04);">
<div style="text-align:center;"><div style="font-family:'Cinzel',serif;font-size:30px;color:#d8c28a;line-height:1;">12</div><div style="font-family:'Jost',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#9a9086;margin-top:5px;">Jun 2026</div></div>
<div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:7px;"><span style="font-family:'Jost',sans-serif;font-size:10px;letter-spacing:0.12em;text-transform:uppercase;color:#150f08;background:#c2a35a;padding:2px 8px;border-radius:2px;font-weight:600;">Announcement</span></div>
<div style="font-family:'Cinzel',serif;font-size:21px;color:#e6e0d6;margin-bottom:6px;">New Player Onboarding: The Watch Welcomes You</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.6;color:#9a9086;margin:0;">A streamlined path from download to your first night in the city — mentors now available on arrival.</p>
</div>
<div style="font-family:'Jost',sans-serif;font-size:22px;color:#6b6151;"></div>
</a>
<a href="https://westgate.pw/category/1/news" style="display:grid;grid-template-columns:130px 1fr auto;gap:28px;align-items:center;padding:26px 8px;border-top:1px solid rgba(194,163,90,0.12);border-bottom:1px solid rgba(194,163,90,0.12);transition:background .2s;" style-hover="background:rgba(194,163,90,0.04);">
<div style="text-align:center;"><div style="font-family:'Cinzel',serif;font-size:30px;color:#d8c28a;line-height:1;">06</div><div style="font-family:'Jost',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#9a9086;margin-top:5px;">Jun 2026</div></div>
<div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:7px;"><span style="font-family:'Jost',sans-serif;font-size:10px;letter-spacing:0.12em;text-transform:uppercase;color:#bf8a86;border:1px solid rgba(166,74,78,0.4);padding:2px 8px;border-radius:2px;">Maintenance</span></div>
<div style="font-family:'Cinzel',serif;font-size:21px;color:#e6e0d6;margin-bottom:6px;">Scheduled Module Reset — June 28</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.6;color:#9a9086;margin:0;">The server will be briefly unavailable for a planned reset. Expect roughly thirty minutes of downtime.</p>
</div>
<div style="font-family:'Jost',sans-serif;font-size:22px;color:#6b6151;"></div>
</a>
</div>
</div>
</sc-if>
<!-- ================= DEV BLOG ================= -->
<sc-if value="{{ isBlog }}" hint-placeholder-val="{{ false }}">
<div style="animation:fadeUp .5s ease both;max-width:880px;margin:0 auto;padding:72px 32px 100px;">
<div style="text-align:center;margin-bottom:60px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.3em;text-transform:uppercase;color:#c2a35a;margin-bottom:14px;">From the Workshop</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:54px;color:#f3ede4;margin:0;letter-spacing:0.03em;">Developer Blog</h1>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:18px;color:#9a9086;margin:18px auto 0;max-width:540px;">Behind-the-scenes development updates — the craft, the decisions, and the long work of building a living city.</p>
</div>
<div style="position:relative;padding-left:38px;">
<div style="position:absolute;left:8px;top:8px;bottom:8px;width:1px;background:linear-gradient(180deg,rgba(194,163,90,0.5),rgba(194,163,90,0.08));"></div>
<article style="position:relative;margin-bottom:48px;">
<div style="position:absolute;left:-34px;top:6px;width:13px;height:13px;border:1.5px solid #c2a35a;background:#100b18;transform:rotate(45deg);"></div>
<div style="background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.16);border-radius:6px;overflow:hidden;">
<div style="height:200px;background:repeating-linear-gradient(135deg,#241a30,#241a30 13px,#1a1326 13px,#1a1326 26px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(194,163,90,0.12);"><span style="font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:0.15em;color:#6b6152;text-transform:uppercase;">screenshot / wip render</span></div>
<div style="padding:34px 38px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
<span style="font-family:'Jost',sans-serif;font-size:10.5px;letter-spacing:0.1em;text-transform:uppercase;color:#bf8a86;border:1px solid rgba(166,74,78,0.4);padding:3px 10px;border-radius:2px;">Worldbuilding</span>
<span style="font-family:'Jost',sans-serif;font-size:12px;color:#9a9086;">22 June 2026 · 7 min read</span>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:27px;line-height:1.25;color:#f3ede4;margin:0 0 16px;">Building the Undergate: Tunnels Beneath the City</h2>
<p style="font-family:'Jost',sans-serif;font-size:16px;line-height:1.9;color:#9a9086;margin:0 0 22px;">Westgate has always been a city of two halves — the lantern-lit streets above, and the flooded warrens below. This month we walk you through how the Undergate came together: the layout philosophy, the lighting passes that took far longer than we'd like to admit, and the factions that now call the dark home.</p>
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#3a1830,#1a1322);border:1px solid rgba(194,163,90,0.3);"></div>
<span style="font-family:'Jost',sans-serif;font-size:13px;color:#b9b2a6;">archvillainette</span>
<span style="color:#6b6152;">·</span>
<a href="https://westgate.pw/category/84/developer-blog" style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.08em;text-transform:uppercase;color:#c2a35a;">Read Entry →</a>
</div>
</div>
</div>
</article>
<article style="position:relative;margin-bottom:48px;">
<div style="position:absolute;left:-34px;top:6px;width:13px;height:13px;border:1.5px solid #c2a35a;background:#100b18;transform:rotate(45deg);"></div>
<div style="background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.16);border-radius:6px;padding:30px 36px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
<span style="font-family:'Jost',sans-serif;font-size:10.5px;letter-spacing:0.1em;text-transform:uppercase;color:#c2a35a;border:1px solid rgba(194,163,90,0.4);padding:3px 10px;border-radius:2px;">Systems</span>
<span style="font-family:'Jost',sans-serif;font-size:12px;color:#9a9086;">9 June 2026 · 6 min read</span>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:25px;line-height:1.25;color:#f3ede4;margin:0 0 14px;">Faction Systems & the Masked Guilds</h2>
<p style="font-family:'Jost',sans-serif;font-size:15.5px;line-height:1.85;color:#9a9086;margin:0 0 20px;">Reputation, favour, and betrayal — how the guild standing system tracks the webs of loyalty players weave, and why we chose to let almost everything be undone with the right bribe.</p>
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#3a1830,#1a1322);border:1px solid rgba(194,163,90,0.3);"></div>
<span style="font-family:'Jost',sans-serif;font-size:13px;color:#b9b2a6;">The Westgate Team</span>
<span style="color:#6b6152;">·</span>
<a href="https://westgate.pw/category/84/developer-blog" style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.08em;text-transform:uppercase;color:#c2a35a;">Read Entry →</a>
</div>
</div>
</article>
<article style="position:relative;margin-bottom:48px;">
<div style="position:absolute;left:-34px;top:6px;width:13px;height:13px;border:1.5px solid #c2a35a;background:#100b18;transform:rotate(45deg);"></div>
<div style="background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.16);border-radius:6px;padding:30px 36px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
<span style="font-family:'Jost',sans-serif;font-size:10.5px;letter-spacing:0.1em;text-transform:uppercase;color:#bf8a86;border:1px solid rgba(166,74,78,0.4);padding:3px 10px;border-radius:2px;">Art & Atmosphere</span>
<span style="font-family:'Jost',sans-serif;font-size:12px;color:#9a9086;">27 May 2026 · 5 min read</span>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:25px;line-height:1.25;color:#f3ede4;margin:0 0 14px;">Lighting Westgate: A Study in Lanternlight</h2>
<p style="font-family:'Jost',sans-serif;font-size:15.5px;line-height:1.85;color:#9a9086;margin:0 0 20px;">Mood is everything in a city built on shadow. A look at our approach to colour temperature, fog, and the hundreds of placed light sources that make the docks feel cold and the taverns feel like refuge.</p>
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#3a1830,#1a1322);border:1px solid rgba(194,163,90,0.3);"></div>
<span style="font-family:'Jost',sans-serif;font-size:13px;color:#b9b2a6;">archvillainette</span>
<span style="color:#6b6152;">·</span>
<a href="https://westgate.pw/category/84/developer-blog" style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.08em;text-transform:uppercase;color:#c2a35a;">Read Entry →</a>
</div>
</div>
</article>
<article style="position:relative;">
<div style="position:absolute;left:-34px;top:6px;width:13px;height:13px;border:1.5px solid #c2a35a;background:#100b18;transform:rotate(45deg);"></div>
<div style="background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.16);border-radius:6px;padding:30px 36px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
<span style="font-family:'Jost',sans-serif;font-size:10.5px;letter-spacing:0.1em;text-transform:uppercase;color:#c2a35a;border:1px solid rgba(194,163,90,0.4);padding:3px 10px;border-radius:2px;">Design</span>
<span style="font-family:'Jost',sans-serif;font-size:12px;color:#9a9086;">14 May 2026 · 8 min read</span>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:25px;line-height:1.25;color:#f3ede4;margin:0 0 14px;">Why We Chose Persistent Roleplay</h2>
<p style="font-family:'Jost',sans-serif;font-size:15.5px;line-height:1.85;color:#9a9086;margin:0 0 20px;">The first entry. Who we are, what Shadows Over Westgate is trying to be, and the kind of stories we hope you'll tell in its streets.</p>
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#3a1830,#1a1322);border:1px solid rgba(194,163,90,0.3);"></div>
<span style="font-family:'Jost',sans-serif;font-size:13px;color:#b9b2a6;">The Westgate Team</span>
<span style="color:#6b6152;">·</span>
<a href="https://westgate.pw/category/84/developer-blog" style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.08em;text-transform:uppercase;color:#c2a35a;">Read Entry →</a>
</div>
</div>
</article>
</div>
</div>
</sc-if>
<!-- ================= GALLERY ================= -->
<sc-if value="{{ isGallery }}" hint-placeholder-val="{{ false }}">
<div style="animation:fadeUp .5s ease both;max-width:1240px;margin:0 auto;padding:72px 32px 100px;">
<div style="text-align:center;margin-bottom:56px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.3em;text-transform:uppercase;color:#c2a35a;margin-bottom:14px;">The Archive</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:54px;color:#f3ede4;margin:0;letter-spacing:0.03em;">Gallery</h1>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:18px;color:#9a9086;margin:18px auto 0;max-width:540px;">Scenes from the streets of Westgate, captured by its denizens. Drag your own screenshots onto any frame.</p>
</div>
<div style="column-count:3;column-gap:18px;">
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(194,163,90,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g1" shape="rect" placeholder="Drop screenshot" style="width:100%;height:300px;display:block;" hint-size="100%,300px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;">The Undergate at Low Tide</div><div style="font-family:'Jost',sans-serif;font-size:11px;color:#9a9086;margin-top:3px;">@duskwarden</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(194,163,90,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g2" shape="rect" placeholder="Drop screenshot" style="width:100%;height:220px;display:block;" hint-size="100%,220px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;">Rain over the Harbour</div><div style="font-family:'Jost',sans-serif;font-size:11px;color:#9a9086;margin-top:3px;">@archvillainette</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(194,163,90,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g3" shape="rect" placeholder="Drop screenshot" style="width:100%;height:340px;display:block;" hint-size="100%,340px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;">Noble Row, House Malforten</div><div style="font-family:'Jost',sans-serif;font-size:11px;color:#9a9086;margin-top:3px;">@velvethand</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(194,163,90,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g4" shape="rect" placeholder="Drop screenshot" style="width:100%;height:240px;display:block;" hint-size="100%,240px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;">Masks of the Quivering Thumb</div><div style="font-family:'Jost',sans-serif;font-size:11px;color:#9a9086;margin-top:3px;">@silktongue</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(194,163,90,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g5" shape="rect" placeholder="Drop screenshot" style="width:100%;height:300px;display:block;" hint-size="100%,300px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;">The Tankard's Rest</div><div style="font-family:'Jost',sans-serif;font-size:11px;color:#9a9086;margin-top:3px;">@ghostofeltabbar</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(194,163,90,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g6" shape="rect" placeholder="Drop screenshot" style="width:100%;height:230px;display:block;" hint-size="100%,230px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;">Cobbles after Curfew</div><div style="font-family:'Jost',sans-serif;font-size:11px;color:#9a9086;margin-top:3px;">@ashen</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(194,163,90,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g7" shape="rect" placeholder="Drop screenshot" style="width:100%;height:330px;display:block;" hint-size="100%,330px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;">The Sea of Fallen Stars at Dusk</div><div style="font-family:'Jost',sans-serif;font-size:11px;color:#9a9086;margin-top:3px;">@cartographer</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(194,163,90,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g8" shape="rect" placeholder="Drop screenshot" style="width:100%;height:250px;display:block;" hint-size="100%,250px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;">A Bargain Beneath the Lantern</div><div style="font-family:'Jost',sans-serif;font-size:11px;color:#9a9086;margin-top:3px;">@nightjar</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(194,163,90,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g9" shape="rect" placeholder="Drop screenshot" style="width:100%;height:290px;display:block;" hint-size="100%,290px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#e6e0d6;">Something in the Undergate</div><div style="font-family:'Jost',sans-serif;font-size:11px;color:#9a9086;margin-top:3px;">@nameless</div></div>
</div>
</div>
</div>
</sc-if>
<!-- ================= JOIN THE TEAM ================= -->
<sc-if value="{{ isRecruit }}" hint-placeholder-val="{{ false }}">
<div style="animation:fadeUp .5s ease both;">
<!-- HERO -->
<section style="max-width:1240px;margin:0 auto;padding:82px 32px 20px;">
<div style="display:grid;grid-template-columns:1.05fr 1fr;gap:56px;align-items:center;">
<div>
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c2a35a;margin-bottom:22px;">Persistent World · Made by Hand</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:52px;line-height:1.08;letter-spacing:0.02em;color:#f3ede4;margin:0 0 22px;">Come build a world that's a pleasure to work on.</h1>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:18px;line-height:1.7;color:#cfc5b7;margin:0 0 34px;max-width:30em;">Westgate is made by a handful of people who love this stuff — artists, writers, builders, scripters. We've kept the tools pleasant so your time goes where it matters: into the world itself. If you make things, we'd love to meet you.</p>
<div style="display:flex;align-items:center;gap:22px;flex-wrap:wrap;">
<a href="https://westgate.pw/categories" style="font-family:'Jost',sans-serif;font-size:13px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:15px 30px;border-radius:3px;border:1px solid #ddc488;box-shadow:0 4px 22px rgba(168,133,74,0.32);">Say Hello on Discord</a>
</div>
</div>
<div style="background:linear-gradient(100deg, rgba(58,24,48,0.5) 0%, rgba(31,25,39,0.97) 23%, rgba(21,18,27,1) 62%, rgba(15,14,20,0.99) 100%);border:1px solid rgba(194,163,90,0.2);border-radius:6px;overflow:hidden;box-shadow:0 24px 60px rgba(0,0,0,0.5);">
<div style="display:flex;align-items:center;gap:9px;padding:13px 16px;border-bottom:1px solid rgba(194,163,90,0.16);">
<span style="display:flex;gap:6px;"><i style="width:9px;height:9px;border-radius:50%;background:#7fb86a;display:block;"></i><i style="width:9px;height:9px;border-radius:50%;background:#d3b573;display:block;"></i><i style="width:9px;height:9px;border-radius:50%;background:#bf8a86;display:block;"></i></span>
<span style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:#9a9086;margin-left:6px;">westgate — the undergate</span>
</div>
<div style="aspect-ratio:16/11;background:repeating-linear-gradient(135deg,#1b1320,#1b1320 11px,#161019 11px,#161019 22px);display:flex;align-items:center;justify-content:center;">
<span style="font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:#6b6152;background:#100b18;border:1px solid rgba(194,163,90,0.16);padding:7px 13px;border-radius:3px;">screenshot · a corner you'll help build</span>
</div>
</div>
</div>
</section>
<!-- PILLARS -->
<section id="wg-stack" style="max-width:1240px;margin:0 auto;padding:80px 32px 30px;scroll-margin-top:80px;">
<div style="text-align:center;margin-bottom:46px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c2a35a;margin-bottom:12px;">Who We Need</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:36px;color:#e6e0d6;margin:0 0 14px;letter-spacing:0.03em;">Every kind of maker has a place here</h2>
<p style="font-family:'Jost',sans-serif;font-size:16px;line-height:1.7;color:#9a9086;margin:0 auto;max-width:32em;">Code, art, words, world — whatever you make, there's a part of Westgate waiting for it.</p>
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:22px;">
<article style="background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.14);border-radius:5px;overflow:hidden;display:flex;flex-direction:column;">
<div style="aspect-ratio:16/9;background:repeating-linear-gradient(135deg,#1b1320,#1b1320 11px,#161019 11px,#161019 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(194,163,90,0.12);"><span style="font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:#6b6152;">toolset · scripts</span></div>
<div style="padding:26px 26px 28px;">
<h3 style="font-family:'Cinzel',serif;font-size:20px;color:#e6e0d6;margin:0 0 10px;">Tools that stay out of your way</h3>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#9a9086;margin:0;">For builders and scripters: a clean toolset and modern scripting on a fast core — none of NWScript's old quirks to fight. Everything's saved and versioned, so you can experiment without fear of breaking things.</p>
</div>
</article>
<article style="background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.14);border-radius:5px;overflow:hidden;display:flex;flex-direction:column;">
<div style="aspect-ratio:16/9;background:repeating-linear-gradient(135deg,#1b1320,#1b1320 11px,#161019 11px,#161019 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(194,163,90,0.12);"><span style="font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:#6b6152;">models · textures · music</span></div>
<div style="padding:26px 26px 28px;">
<h3 style="font-family:'Cinzel',serif;font-size:20px;color:#e6e0d6;margin:0 0 10px;">Your craft, your stamp</h3>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#9a9086;margin:0;">For artists and asset-makers: models, textures, areas, sound. We're not after a house style to copy — we want the thing only you'd make, and we'll help get it into the game.</p>
</div>
</article>
<article style="background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.14);border-radius:5px;overflow:hidden;display:flex;flex-direction:column;">
<div style="aspect-ratio:16/9;background:repeating-linear-gradient(135deg,#1b1320,#1b1320 11px,#161019 11px,#161019 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(194,163,90,0.12);"><span style="font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:#6b6152;">lore · quests · factions</span></div>
<div style="padding:26px 26px 28px;">
<h3 style="font-family:'Cinzel',serif;font-size:20px;color:#e6e0d6;margin:0 0 10px;">Ideas that reach players</h3>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#9a9086;margin:0;">For writers and designers: a questline, a faction, a forgotten corner of the city. What you dream up this month is something players are living in the next.</p>
</div>
</article>
</div>
</section>
<!-- TEAM -->
<section style="max-width:1240px;margin:0 auto;padding:56px 32px 30px;">
<div style="background:linear-gradient(100deg, rgba(58,24,48,0.5) 0%, rgba(31,25,39,0.97) 23%, rgba(21,18,27,1) 62%, rgba(15,14,20,0.99) 100%);border:1px solid rgba(194,163,90,0.2);border-radius:6px;box-shadow:0 20px 50px rgba(0,0,0,0.4);padding:46px 50px;display:grid;grid-template-columns:1.1fr 1fr;gap:50px;align-items:center;">
<div>
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c2a35a;margin-bottom:14px;">What It's Actually Like</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:30px;line-height:1.2;color:#f3ede4;margin:0 0 16px;">Low ego, high trust — and your work goes live.</h2>
<p style="font-family:'Jost',sans-serif;font-size:16px;line-height:1.85;color:#9a9086;margin:0;">No gatekeeping, no hazing, no “figure it out yourself.” We give honest, kind feedback, lend a hand when it helps, and keep things moving. The thing you make this week is the thing players see this week.</p>
</div>
<div style="display:flex;flex-direction:column;gap:16px;">
<div style="display:flex;align-items:center;gap:14px;font-family:'Jost',sans-serif;font-size:15.5px;color:#b9b2a6;"><span style="width:7px;height:7px;background:#c2a35a;transform:rotate(45deg);flex:none;"></span>Honest, kind feedback on your work</div>
<div style="display:flex;align-items:center;gap:14px;font-family:'Jost',sans-serif;font-size:15.5px;color:#b9b2a6;"><span style="width:7px;height:7px;background:#c2a35a;transform:rotate(45deg);flex:none;"></span>A hand whenever you want one</div>
<div style="display:flex;align-items:center;gap:14px;font-family:'Jost',sans-serif;font-size:15.5px;color:#b9b2a6;"><span style="width:7px;height:7px;background:#c2a35a;transform:rotate(45deg);flex:none;"></span>What you make can be in-game within the week</div>
<div style="display:flex;align-items:center;gap:14px;font-family:'Jost',sans-serif;font-size:15.5px;color:#b9b2a6;"><span style="width:7px;height:7px;background:#c2a35a;transform:rotate(45deg);flex:none;"></span>Work at whatever pace fits your life</div>
</div>
</div>
</section>
<!-- CTA -->
<section style="max-width:760px;margin:0 auto;padding:70px 32px 100px;text-align:center;">
<div style="display:flex;align-items:center;justify-content:center;gap:18px;margin-bottom:40px;">
<div style="height:1px;width:80px;background:linear-gradient(90deg,transparent,#c2a35a);"></div>
<div style="width:7px;height:7px;background:#c2a35a;transform:rotate(45deg);"></div>
<div style="height:1px;width:80px;background:linear-gradient(90deg,#c2a35a,transparent);"></div>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:700;font-size:40px;color:#f3ede4;margin:0 0 16px;letter-spacing:0.02em;">Sound like your kind of place?</h2>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:18px;line-height:1.7;color:#cfc5b7;margin:0 0 30px;">No test gauntlet, no résumé. Just come say hi and tell us what you'd love to build.</p>
<!-- ponytail: point at the real Discord invite when you have it -->
<a href="https://westgate.pw/categories" style="display:inline-block;font-family:'Jost',sans-serif;font-size:13px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:16px 36px;border-radius:3px;border:1px solid #ddc488;box-shadow:0 4px 26px rgba(168,133,74,0.4);">Join the Team on Discord →</a>
<div style="margin-top:46px;padding-top:22px;border-top:1px solid rgba(194,163,90,0.12);font-family:'Jost',sans-serif;font-size:12px;letter-spacing:0.08em;color:#7f756f;">Artists · writers · builders · scripters — all welcome</div>
</section>
</div>
</sc-if>
<!-- ================= ARTICLE (topic rendered as banner-led article) ================= -->
<sc-if value="{{ isArticle }}" hint-placeholder-val="{{ false }}">
<div style="animation:fadeUp .5s ease both;">
<!-- BANNER HERO (pulled from topic thumbnail / first image) -->
<section style="max-width:1180px;margin:14px auto 0;padding:0 32px;">
<div style="position:relative;overflow:hidden;border:1px solid rgba(194,163,90,0.22);border-radius:10px;min-height:420px;display:flex;flex-direction:column;justify-content:flex-end;box-shadow:inset 0 1px 0 rgba(255,242,204,0.06),0 18px 42px rgba(0,0,0,0.3);">
<div style="position:absolute;inset:0;background:repeating-linear-gradient(135deg,#241a30,#241a30 13px,#1a1326 13px,#1a1326 26px);"></div>
<div style="position:absolute;inset:0;background:linear-gradient(180deg,rgba(9,8,11,0.2) 0%,rgba(9,8,11,0.55) 55%,rgba(9,8,11,0.94) 100%);"></div>
<div style="position:absolute;top:18px;left:20px;font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:#6b6152;background:rgba(9,8,11,0.6);border:1px solid rgba(194,163,90,0.16);padding:6px 11px;border-radius:3px;">topic banner · first image from post</div>
<div style="position:relative;padding:46px 50px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
<span style="font-family:'Jost',sans-serif;font-size:10.5px;letter-spacing:0.12em;text-transform:uppercase;color:#150f08;background:#c2a35a;padding:4px 11px;border-radius:2px;font-weight:600;">Announcement</span>
<span style="font-family:'IBM Plex Mono',monospace;font-size:12px;letter-spacing:0.06em;color:#b9b2a6;">24 June 2026</span>
</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:clamp(34px,4.4vw,52px);line-height:1.08;letter-spacing:0.02em;color:#f3ede4;margin:0;max-width:18em;text-shadow:0 4px 30px rgba(0,0,0,0.7);">The Gates of Westgate Open — Alpha Has Begun</h1>
<div style="display:flex;align-items:center;gap:13px;margin-top:22px;">
<div style="width:34px;height:34px;border-radius:50%;background:linear-gradient(135deg,#3a1830,#1a1322);border:1px solid rgba(194,163,90,0.3);"></div>
<span style="font-family:'Jost',sans-serif;font-size:14px;color:#cfc5b7;">The Watch</span>
<span style="color:#6b6152;">·</span>
<span style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:#9a9086;">4 min read</span>
</div>
</div>
</div>
</section>
<!-- BREADCRUMB -->
<section style="max-width:760px;margin:0 auto;padding:26px 32px 0;">
<div style="font-family:'Cinzel',serif;font-size:13px;font-weight:600;color:#9a9086;display:flex;align-items:center;gap:12px;">
<span onClick="{{ goNews }}" style="cursor:pointer;" style-hover="color:#e0c878;">News</span>
<span style="color:#a8893f;"></span>
<span style="color:#b9b2a6;">Alpha Has Begun</span>
</div>
</section>
<!-- ARTICLE PROSE (uses the wiki-article-prose vocabulary in production) -->
<article style="max-width:760px;margin:0 auto;padding:30px 32px 20px;font-family:'Jost',sans-serif;">
<p style="font-size:20px;line-height:1.75;color:#cfc5b7;margin:0 0 28px;font-weight:300;">After long months in the shipyards, the harbour is open at last. Westgate welcomes its first denizens to walk the salt-worn docks, strike bargains beneath the lanterns, and write the first chapters of the city's living history.</p>
<p style="font-size:17px;line-height:1.95;color:#b9b2a6;margin:0 0 22px;">This is the moment we've been building toward. The gates are unbarred, the lanterns are lit, and the streets are yours to walk. Whether you come seeking fortune, power, or simply somewhere to disappear, there is a place for you among the shadows — and a price for everything, as there always is in Westgate.</p>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:27px;color:#c2a35a;margin:42px 0 8px;padding-bottom:8px;position:relative;">What to Expect<span style="position:absolute;left:0;bottom:0;width:min(14rem,48%);height:1px;background:linear-gradient(90deg,rgba(194,163,90,0.42),rgba(194,163,90,0.1) 54%,transparent);"></span></h2>
<p style="font-size:17px;line-height:1.95;color:#b9b2a6;margin:0 0 22px;">The world is alpha — rough in places, and all the better for your fingerprints on it. Expect active development, frequent patches, and a team that listens. Your characters, your stories, and your feedback shape what Westgate becomes.</p>
<blockquote style="font-family:'Cinzel',serif;font-style:italic;font-size:21px;line-height:1.55;color:#d8c28a;margin:34px 0;padding:8px 0 8px 28px;border-left:2px solid rgba(194,163,90,0.4);background:rgba(35,27,23,0.36);">In Westgate, few leave unchanged… and fewer still leave at all.</blockquote>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:27px;color:#c2a35a;margin:42px 0 8px;padding-bottom:8px;position:relative;">How to Begin<span style="position:absolute;left:0;bottom:0;width:min(14rem,48%);height:1px;background:linear-gradient(90deg,rgba(194,163,90,0.42),rgba(194,163,90,0.1) 54%,transparent);"></span></h2>
<p style="font-size:17px;line-height:1.95;color:#b9b2a6;margin:0 0 22px;">Install Neverwinter Nights: Enhanced Edition, follow the connect guide on the wiki, and create your character. A mentor from the Watch will meet you at the gate. Mind the shadows — and welcome.</p>
</article>
<!-- ARTICLE FOOTER: discuss on forum -->
<section style="max-width:760px;margin:0 auto;padding:18px 32px 0;">
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:16px;padding:22px 26px;border:1px solid rgba(194,163,90,0.14);border-radius:8px;background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);box-shadow:inset 0 1px 0 rgba(255,255,255,0.025),0 10px 26px rgba(0,0,0,0.24);">
<div style="font-family:'Jost',sans-serif;font-size:15px;color:#b9b2a6;">This is a live forum topic — replies appear below it on the real page.</div>
<a href="https://westgate.pw/category/1/news" style="font-family:'Jost',sans-serif;font-size:12.5px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d8bd76,#a8893f);padding:11px 22px;border-radius:6px;border:1px solid rgba(216,194,138,0.6);white-space:nowrap;">Join the Discussion →</a>
</div>
</section>
<!-- MORE FROM NEWS -->
<section style="max-width:1180px;margin:0 auto;padding:64px 32px 100px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c2a35a;margin-bottom:24px;text-align:center;">More Dispatches</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:22px;">
<div onClick="{{ goArticle }}" style="cursor:pointer;background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.14);border-radius:8px;overflow:hidden;box-shadow:inset 0 1px 0 rgba(255,255,255,0.025),0 10px 26px rgba(0,0,0,0.24);">
<div style="height:140px;background:repeating-linear-gradient(135deg,#1b1320,#1b1320 11px,#161019 11px,#161019 22px);border-bottom:1px solid rgba(194,163,90,0.12);"></div>
<div style="padding:20px 22px;"><div style="font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:#9a9086;margin-bottom:9px;">21 Jun · Patch Note</div><div style="font-family:'Cinzel',serif;font-size:18px;line-height:1.35;color:#e6e0d6;">Patch 0.3 — The Undergate</div></div>
</div>
<div onClick="{{ goArticle }}" style="cursor:pointer;background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.14);border-radius:8px;overflow:hidden;box-shadow:inset 0 1px 0 rgba(255,255,255,0.025),0 10px 26px rgba(0,0,0,0.24);">
<div style="height:140px;background:repeating-linear-gradient(135deg,#1b1320,#1b1320 11px,#161019 11px,#161019 22px);border-bottom:1px solid rgba(194,163,90,0.12);"></div>
<div style="padding:20px 22px;"><div style="font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:#9a9086;margin-bottom:9px;">18 Jun · Community</div><div style="font-family:'Cinzel',serif;font-size:18px;line-height:1.35;color:#e6e0d6;">The Community Charter</div></div>
</div>
<div onClick="{{ goArticle }}" style="cursor:pointer;background:linear-gradient(100deg, rgba(42,18,34,0.46) 0%, rgba(25,22,31,0.95) 23%, rgba(18,16,23,0.99) 62%, rgba(13,12,17,0.98) 100%);border:1px solid rgba(194,163,90,0.14);border-radius:8px;overflow:hidden;box-shadow:inset 0 1px 0 rgba(255,255,255,0.025),0 10px 26px rgba(0,0,0,0.24);">
<div style="height:140px;background:repeating-linear-gradient(135deg,#1b1320,#1b1320 11px,#161019 11px,#161019 22px);border-bottom:1px solid rgba(194,163,90,0.12);"></div>
<div style="padding:20px 22px;"><div style="font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:#9a9086;margin-bottom:9px;">06 Jun · Maintenance</div><div style="font-family:'Cinzel',serif;font-size:18px;line-height:1.35;color:#e6e0d6;">Scheduled Module Reset</div></div>
</div>
</div>
</section>
</div>
</sc-if>
<!-- ================= FOOTER ================= -->
<footer style="border-top:1px solid rgba(194,163,90,0.16);background:linear-gradient(180deg,rgba(13,9,18,0),rgba(8,5,12,0.6));margin-top:20px;">
<div style="max-width:1240px;margin:0 auto;padding:64px 32px 30px;display:grid;grid-template-columns:1.6fr 1fr 1fr;gap:50px;">
<div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:18px;">
<div style="width:22px;height:22px;transform:rotate(45deg);border:1.5px solid #c2a35a;display:flex;align-items:center;justify-content:center;"><div style="width:7px;height:7px;background:#c2a35a;"></div></div>
<div style="font-family:'Cinzel',serif;font-weight:600;font-size:15px;letter-spacing:0.14em;color:#e6e0d6;">SHADOWS OVER WESTGATE</div>
</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.8;color:#9a9086;margin:0 0 18px;max-width:380px;">A Neverwinter Nights: Enhanced Edition persistent world of gothic intrigue, set in the Forgotten Realms port city of Westgate.</p>
<p style="font-family:'Cinzel',serif;font-style:italic;font-size:14px;color:#9a8262;margin:0;">Mind the shadows.</p>
</div>
<div>
<div style="font-family:'Jost',sans-serif;font-size:11px;letter-spacing:0.16em;text-transform:uppercase;color:#c2a35a;margin-bottom:18px;">Explore</div>
<div style="display:flex;flex-direction:column;gap:12px;">
<div onClick="{{ goHome }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:14.5px;color:#9a9086;" style-hover="color:#e6e0d6;">Home</div>
<div onClick="{{ goNews }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:14.5px;color:#9a9086;" style-hover="color:#e6e0d6;">News</div>
<div onClick="{{ goGallery }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:14.5px;color:#9a9086;" style-hover="color:#e6e0d6;">Gallery</div>
<div onClick="{{ goBlog }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:14.5px;color:#9a9086;" style-hover="color:#e6e0d6;">Dev Blog</div>
<div onClick="{{ goRecruit }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:14.5px;color:#9a9086;" style-hover="color:#e6e0d6;">Join the Team</div>
</div>
</div>
<div>
<div style="font-family:'Jost',sans-serif;font-size:11px;letter-spacing:0.16em;text-transform:uppercase;color:#c2a35a;margin-bottom:18px;">Community</div>
<div style="display:flex;flex-direction:column;gap:12px;">
<a href="https://westgate.pw/categories" style="font-family:'Jost',sans-serif;font-size:14.5px;color:#9a9086;" style-hover="color:#e6e0d6;">Forums</a>
<a href="https://westgate.pw/wiki" style="font-family:'Jost',sans-serif;font-size:14.5px;color:#9a9086;" style-hover="color:#e6e0d6;">Wiki</a>
<a href="https://westgate.pw/register" style="font-family:'Jost',sans-serif;font-size:14.5px;color:#9a9086;" style-hover="color:#e6e0d6;">Register</a>
<a href="https://westgate.pw/wiki" style="font-family:'Jost',sans-serif;font-size:14.5px;color:#9a9086;" style-hover="color:#e6e0d6;">How to Join</a>
</div>
</div>
</div>
<div style="max-width:1240px;margin:0 auto;padding:22px 32px 40px;border-top:1px solid rgba(194,163,90,0.1);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;">
<div style="font-family:'Jost',sans-serif;font-size:12px;color:#7f756f;">© 2026 Shadows Over Westgate · A NWN:EE Persistent World</div>
<div style="font-family:'Jost',sans-serif;font-size:12px;color:#7f756f;">Forgotten Realms · Westgate · Sea of Fallen Stars</div>
</div>
</footer>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:1280,&quot;height&quot;:860},&quot;startPage&quot;:{&quot;editor&quot;:&quot;enum&quot;,&quot;options&quot;:[&quot;home&quot;,&quot;news&quot;,&quot;blog&quot;,&quot;gallery&quot;,&quot;recruit&quot;,&quot;article&quot;],&quot;default&quot;:&quot;home&quot;,&quot;tsType&quot;:&quot;'home'|'news'|'blog'|'gallery'|'recruit'|'article'&quot;}}">
class Component extends DCLogic {
state = { page: this.props.startPage || 'home' };
go(page) {
this.setState({ page });
if (typeof window !== 'undefined') {
try { window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (e) { window.scrollTo(0, 0); }
}
}
renderVals() {
const p = this.state.page;
return {
isHome: p === 'home',
isNews: p === 'news',
isBlog: p === 'blog',
isGallery: p === 'gallery',
isRecruit: p === 'recruit',
isArticle: p === 'article',
goHome: () => this.go('home'),
goArticle: () => this.go('article'),
goNews: () => this.go('news'),
goBlog: () => this.go('blog'),
goGallery: () => this.go('gallery'),
goRecruit: () => this.go('recruit'),
};
}
}
</script>
</body>
</html>
+643
View File
@@ -0,0 +1,643 @@
// @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design)
/* BEGIN USAGE */
/**
* <image-slot> — user-fillable image placeholder.
*
* Drop this into a deck, mockup, or page wherever you want the user to
* supply an image. You control the slot's shape and size; the user fills it
* by dragging an image file onto it (or clicking to browse). The dropped
* image persists across reloads via a .image-slots.state.json sidecar —
* same read-via-fetch / write-via-window.omelette pattern as
* design_canvas.jsx, so the filled slot shows on share links, downloaded
* zips, and PPTX export. Outside the omelette runtime the slot is read-only.
*
* The host bridge only allows sidecar writes at the project root, so the
* HTML that uses this component is assumed to live at the project root too
* (same constraint as design_canvas.jsx).
*
* Attributes:
* id Persistence key. REQUIRED for the drop to survive reload —
* every slot on the page needs a distinct id.
* shape 'rect' | 'rounded' | 'circle' | 'pill' (default 'rounded')
* 'circle' applies 50% border-radius; on a non-square slot
* that's an ellipse — set equal width and height for a true
* circle.
* radius Corner radius in px for 'rounded'. (default 12)
* mask Any CSS clip-path value. Overrides `shape` — use this for
* hexagons, blobs, arbitrary polygons.
* fit object-fit: cover | contain | fill. (default 'cover')
* With cover (the default) double-clicking the filled slot
* enters a reframe mode: the whole image spills past the mask
* (translucent outside, opaque inside), drag to reposition,
* corner-drag to scale. The crop persists alongside the image
* in the sidecar. contain/fill stay static.
* position object-position for fit=contain|fill. (default '50% 50%')
* placeholder Empty-state caption. (default 'Drop an image')
* src Optional initial/fallback image URL. A user drop overrides
* it; clearing the drop reveals src again.
*
* Size and layout come from ordinary CSS on the element — width/height
* inline or from a parent grid — so it composes with any layout.
*
* Usage:
* <image-slot id="hero" style="width:800px;height:450px" shape="rounded" radius="20"
* placeholder="Drop a hero image"></image-slot>
* <image-slot id="avatar" style="width:120px;height:120px" shape="circle"></image-slot>
* <image-slot id="kite" style="width:300px;height:300px"
* mask="polygon(50% 0, 100% 50%, 50% 100%, 0 50%)"></image-slot>
*/
/* END USAGE */
(() => {
const STATE_FILE = '.image-slots.state.json';
// 2× a ~600px slot in a 1920-wide deck — retina-sharp without making the
// sidecar enormous. A 1200px WebP at q=0.85 is ~150-300KB.
const MAX_DIM = 1200;
// Raster formats only. SVG is excluded (can carry script; createImageBitmap
// on SVG blobs is inconsistent). GIF is excluded because the canvas
// re-encode keeps only the first frame, so an animated GIF would silently
// go still — better to reject than surprise.
const ACCEPT = ['image/png', 'image/jpeg', 'image/webp', 'image/avif'];
// ── Shared sidecar store ────────────────────────────────────────────────
// One fetch + immediate write-on-change for every <image-slot> on the
// page. Reads via fetch() so viewing works anywhere the HTML and sidecar
// are served together; writes go through window.omelette.writeFile, which
// the host allowlists to *.state.json basenames only.
const subs = new Set();
let slots = {};
// ids explicitly cleared before the sidecar fetch resolved — otherwise
// the merge below can't tell "never set" from "just deleted" and would
// resurrect the sidecar's stale value.
const tombstones = new Set();
let loaded = false;
let loadP = null;
function load() {
if (loadP) return loadP;
loadP = fetch(STATE_FILE)
.then((r) => (r.ok ? r.json() : null))
.then((j) => {
// Merge: sidecar loses to any in-memory change that raced ahead of
// the fetch (drop or clear) so neither is clobbered by hydration.
if (j && typeof j === 'object') {
const merged = Object.assign({}, j, slots);
// A framing-only write that raced ahead of hydration must not
// drop a user image that's only on disk — inherit u from the
// sidecar for any in-memory entry that lacks one.
for (const k in slots) {
if (merged[k] && !merged[k].u && j[k]) {
merged[k].u = typeof j[k] === 'string' ? j[k] : j[k].u;
}
}
for (const id of tombstones) delete merged[id];
slots = merged;
}
tombstones.clear();
})
.catch(() => {})
.then(() => { loaded = true; subs.forEach((fn) => fn()); });
return loadP;
}
// Serialize writes so two near-simultaneous drops on different slots
// can't reorder at the backend and leave the sidecar with only the
// first. A save requested mid-flight just marks dirty and re-fires on
// completion with the then-current slots.
let saving = false;
let saveDirty = false;
function save() {
if (saving) { saveDirty = true; return; }
const w = window.omelette && window.omelette.writeFile;
if (!w) return;
saving = true;
Promise.resolve(w(STATE_FILE, JSON.stringify(slots)))
.catch(() => {})
.then(() => { saving = false; if (saveDirty) { saveDirty = false; save(); } });
}
const S_MAX = 5;
const clampS = (s) => Math.max(1, Math.min(S_MAX, s));
// Normalize a stored slot value. Pre-reframe sidecars stored a bare
// data-URL string; newer ones store {u, s, x, y}. Either shape is valid.
function getSlot(id) {
const v = slots[id];
if (!v) return null;
return typeof v === 'string' ? { u: v, s: 1, x: 0, y: 0 } : v;
}
function setSlot(id, val) {
if (!id) return;
if (val) { slots[id] = val; tombstones.delete(id); }
else { delete slots[id]; if (!loaded) tombstones.add(id); }
subs.forEach((fn) => fn());
// A drop is rare + high-value — write immediately so nav-away can't lose
// it. Gate on the initial read so we don't overwrite a sidecar we haven't
// merged yet; the merge in load() keeps this change once the read lands.
if (loaded) save(); else load().then(save);
}
// ── Image downscale ─────────────────────────────────────────────────────
// Encode through a canvas so the sidecar carries resized bytes, not the
// raw upload. Longest side is capped at 2× the slot's rendered width
// (retina) and at MAX_DIM. WebP keeps alpha and is ~10× smaller than PNG
// for photos, so there's no need for per-image format picking.
async function toDataUrl(file, targetW) {
const bitmap = await createImageBitmap(file);
try {
const cap = Math.min(MAX_DIM, Math.max(1, Math.round(targetW * 2)) || MAX_DIM);
const scale = Math.min(1, cap / Math.max(bitmap.width, bitmap.height));
const w = Math.max(1, Math.round(bitmap.width * scale));
const h = Math.max(1, Math.round(bitmap.height * scale));
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
canvas.getContext('2d').drawImage(bitmap, 0, 0, w, h);
return canvas.toDataURL('image/webp', 0.85);
} finally {
bitmap.close && bitmap.close();
}
}
// ── Custom element ──────────────────────────────────────────────────────
const stylesheet =
':host{display:inline-block;position:relative;vertical-align:top;' +
' font:13px/1.3 system-ui,-apple-system,sans-serif;color:rgba(0,0,0,.55);width:240px;height:160px}' +
'.frame{position:absolute;inset:0;overflow:hidden;background:rgba(0,0,0,.04)}' +
// .frame img (clipped) and .spill (unclipped ghost + handles) share the
// same left/top/width/height in frame-%, computed by _applyView(), so the
// inside-mask crop and the outside-mask spill stay pixel-aligned.
'.frame img{position:absolute;max-width:none;transform:translate(-50%,-50%);' +
' -webkit-user-drag:none;user-select:none;touch-action:none}' +
// Reframe mode (double-click): the full image spills past the mask. The
// spill layer is sized to the IMAGE bounds so its corners are where the
// resize handles belong. The ghost <img> inside is translucent; the real
// clipped <img> underneath shows the opaque in-mask crop.
'.spill{position:absolute;transform:translate(-50%,-50%);display:none;z-index:1;' +
' cursor:grab;touch-action:none}' +
':host([data-panning]) .spill{cursor:grabbing}' +
'.spill .ghost{position:absolute;inset:0;width:100%;height:100%;opacity:.35;' +
' pointer-events:none;-webkit-user-drag:none;user-select:none;' +
' box-shadow:0 0 0 1px rgba(0,0,0,.2),0 12px 32px rgba(0,0,0,.2)}' +
'.spill .handle{position:absolute;width:12px;height:12px;border-radius:50%;' +
' background:#fff;box-shadow:0 0 0 1.5px #c96442,0 1px 3px rgba(0,0,0,.3);' +
' transform:translate(-50%,-50%)}' +
'.spill .handle[data-c=nw]{left:0;top:0;cursor:nwse-resize}' +
'.spill .handle[data-c=ne]{left:100%;top:0;cursor:nesw-resize}' +
'.spill .handle[data-c=sw]{left:0;top:100%;cursor:nesw-resize}' +
'.spill .handle[data-c=se]{left:100%;top:100%;cursor:nwse-resize}' +
':host([data-reframe]){z-index:10}' +
':host([data-reframe]) .spill{display:block}' +
':host([data-reframe]) .frame{box-shadow:0 0 0 2px #c96442}' +
'.empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;' +
' justify-content:center;gap:6px;text-align:center;padding:12px;box-sizing:border-box;' +
' cursor:pointer;user-select:none}' +
'.empty svg{opacity:.45}' +
'.empty .cap{max-width:90%;font-weight:500;letter-spacing:.01em}' +
'.empty .sub{font-size:11px}' +
'.empty .sub u{text-underline-offset:2px;text-decoration-color:rgba(0,0,0,.25)}' +
'.empty:hover .sub u{color:rgba(0,0,0,.75);text-decoration-color:currentColor}' +
':host([data-over]) .frame{outline:2px solid #c96442;outline-offset:-2px;' +
' background:rgba(201,100,66,.10)}' +
'.ring{position:absolute;inset:0;pointer-events:none;border:1.5px dashed rgba(0,0,0,.25);' +
' transition:border-color .12s}' +
':host([data-over]) .ring{border-color:#c96442}' +
':host([data-filled]) .ring{display:none}' +
// Controls sit BELOW the mask (top:100%), absolutely positioned so the
// author-declared slot height is unaffected. The gap is padding, not a
// top offset, so the hover target stays contiguous with the frame.
'.ctl{position:absolute;top:100%;left:50%;transform:translateX(-50%);padding-top:8px;' +
' display:flex;gap:6px;opacity:0;pointer-events:none;transition:opacity .12s;z-index:2;' +
' white-space:nowrap}' +
':host([data-filled][data-editable]:hover) .ctl,:host([data-reframe]) .ctl' +
' {opacity:1;pointer-events:auto}' +
'.ctl button{appearance:none;border:0;border-radius:6px;padding:5px 10px;cursor:pointer;' +
' background:rgba(0,0,0,.65);color:#fff;font:11px/1 system-ui,-apple-system,sans-serif;' +
' backdrop-filter:blur(6px)}' +
'.ctl button:hover{background:rgba(0,0,0,.8)}' +
'.err{position:absolute;left:8px;bottom:8px;right:8px;color:#b3261e;font-size:11px;' +
' background:rgba(255,255,255,.85);padding:4px 6px;border-radius:5px;pointer-events:none}';
const icon =
'<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" ' +
'stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">' +
'<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/>' +
'<path d="m21 15-5-5L5 21"/></svg>';
class ImageSlot extends HTMLElement {
static get observedAttributes() {
return ['shape', 'radius', 'mask', 'fit', 'position', 'placeholder', 'src', 'id'];
}
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
// .spill and .ctl sit OUTSIDE .frame so overflow:hidden + border-radius
// on the frame (circle, pill, rounded) can't clip them.
root.innerHTML =
'<style>' + stylesheet + '</style>' +
'<div class="frame" part="frame">' +
' <img part="image" alt="" draggable="false" style="display:none">' +
' <div class="empty" part="empty">' + icon +
' <div class="cap"></div>' +
' <div class="sub">or <u>browse files</u></div></div>' +
' <div class="ring" part="ring"></div>' +
'</div>' +
'<div class="spill">' +
' <img class="ghost" alt="" draggable="false">' +
' <div class="handle" data-c="nw"></div><div class="handle" data-c="ne"></div>' +
' <div class="handle" data-c="sw"></div><div class="handle" data-c="se"></div>' +
'</div>' +
'<div class="ctl"><button data-act="replace" title="Replace image">Replace</button>' +
' <button data-act="clear" title="Remove image">Remove</button></div>' +
'<input type="file" accept="' + ACCEPT.join(',') + '" hidden>';
this._frame = root.querySelector('.frame');
this._ring = root.querySelector('.ring');
this._img = root.querySelector('.frame img');
this._empty = root.querySelector('.empty');
this._cap = root.querySelector('.cap');
this._sub = root.querySelector('.sub');
this._spill = root.querySelector('.spill');
this._ghost = root.querySelector('.ghost');
this._err = null;
this._input = root.querySelector('input');
this._depth = 0;
this._gen = 0;
this._view = { s: 1, x: 0, y: 0 };
this._subFn = () => this._render();
// Shadow-DOM listeners live with the shadow DOM — bound once here so
// disconnect/reconnect (e.g. React remount) doesn't stack handlers.
this._empty.addEventListener('click', () => this._input.click());
root.addEventListener('click', (e) => {
const act = e.target && e.target.getAttribute && e.target.getAttribute('data-act');
if (act === 'replace') { this._exitReframe(true); this._input.click(); }
if (act === 'clear') {
this._exitReframe(false);
this._gen++;
this._local = null;
if (this.id) setSlot(this.id, null); else this._render();
}
});
this._input.addEventListener('change', () => {
const f = this._input.files && this._input.files[0];
if (f) this._ingest(f);
this._input.value = '';
});
// naturalWidth/Height aren't known until load — re-apply so the cover
// baseline is computed from real dimensions, not the 100%×100% fallback.
this._img.addEventListener('load', () => this._applyView());
// Gated on editable + fit=cover so share links and contain/fill slots
// stay static.
this.addEventListener('dblclick', (e) => {
if (!this.hasAttribute('data-editable') || !this._reframes()) return;
e.preventDefault();
if (this.hasAttribute('data-reframe')) this._exitReframe(true);
else this._enterReframe();
});
// Pan + resize both originate on the spill layer. A handle pointerdown
// drives an aspect-locked resize anchored at the opposite corner; any
// other pointerdown on the spill pans. Offsets are frame-% so a
// reframed slot survives responsive resize / PPTX export.
this._spill.addEventListener('pointerdown', (e) => {
if (e.button !== 0 || !this.hasAttribute('data-reframe')) return;
e.preventDefault();
e.stopPropagation();
this._spill.setPointerCapture(e.pointerId);
const rect = this.getBoundingClientRect();
const fw = rect.width || 1, fh = rect.height || 1;
const corner = e.target.getAttribute && e.target.getAttribute('data-c');
let move;
if (corner) {
// Resize about the OPPOSITE corner. Viewport-px throughout (rect
// fw/fh, not clientWidth) so the math survives a transform:scale()
// ancestor — deck_stage renders slides scaled-to-fit.
const iw = this._img.naturalWidth || 1, ih = this._img.naturalHeight || 1;
const base = Math.max(fw / iw, fh / ih);
const sx = corner.includes('e') ? 1 : -1;
const sy = corner.includes('s') ? 1 : -1;
const s0 = this._view.s;
const w0 = iw * base * s0, h0 = ih * base * s0;
const cx0 = (50 + this._view.x) / 100 * fw;
const cy0 = (50 + this._view.y) / 100 * fh;
const ox = cx0 - sx * w0 / 2, oy = cy0 - sy * h0 / 2;
const diag0 = Math.hypot(w0, h0);
const ux = sx * w0 / diag0, uy = sy * h0 / diag0;
move = (ev) => {
const proj = (ev.clientX - rect.left - ox) * ux +
(ev.clientY - rect.top - oy) * uy;
const s = clampS(s0 * proj / diag0);
const d = diag0 * s / s0;
this._view.s = s;
this._view.x = (ox + ux * d / 2) / fw * 100 - 50;
this._view.y = (oy + uy * d / 2) / fh * 100 - 50;
this._clampView();
this._applyView();
};
} else {
this.setAttribute('data-panning', '');
const start = { px: e.clientX, py: e.clientY, x: this._view.x, y: this._view.y };
move = (ev) => {
this._view.x = start.x + (ev.clientX - start.px) / fw * 100;
this._view.y = start.y + (ev.clientY - start.py) / fh * 100;
this._clampView();
this._applyView();
};
}
const up = () => {
try { this._spill.releasePointerCapture(e.pointerId); } catch {}
this._spill.removeEventListener('pointermove', move);
this._spill.removeEventListener('pointerup', up);
this._spill.removeEventListener('pointercancel', up);
this.removeAttribute('data-panning');
this._dragUp = null;
};
// Stashed so _exitReframe (Escape / outside-click mid-drag) can
// tear the capture + listeners down synchronously.
this._dragUp = up;
this._spill.addEventListener('pointermove', move);
this._spill.addEventListener('pointerup', up);
this._spill.addEventListener('pointercancel', up);
});
// Wheel zoom stays available inside reframe mode as a trackpad nicety —
// zooms toward the cursor (offset' = cursor·(1-k) + offset·k).
this.addEventListener('wheel', (e) => {
if (!this.hasAttribute('data-reframe')) return;
e.preventDefault();
const r = this.getBoundingClientRect();
const cx = (e.clientX - r.left) / r.width * 100 - 50;
const cy = (e.clientY - r.top) / r.height * 100 - 50;
const prev = this._view.s;
const next = clampS(prev * Math.pow(1.0015, -e.deltaY));
if (next === prev) return;
const k = next / prev;
this._view.s = next;
this._view.x = cx * (1 - k) + this._view.x * k;
this._view.y = cy * (1 - k) + this._view.y * k;
this._clampView();
this._applyView();
}, { passive: false });
}
connectedCallback() {
// Warn once per page — an id-less slot works for the session but
// cannot persist, and two id-less slots would share nothing.
if (!this.id && !ImageSlot._warned) {
ImageSlot._warned = true;
console.warn('<image-slot> without an id will not persist its dropped image.');
}
this.addEventListener('dragenter', this);
this.addEventListener('dragover', this);
this.addEventListener('dragleave', this);
this.addEventListener('drop', this);
subs.add(this._subFn);
// width%/height% in _applyView encode the frame aspect at call time —
// a host resize (responsive grid, pane divider) would stretch the
// image until the next _render. Re-render on size change: _render()
// re-seeds _view from stored before clamp/apply, so a shrink→grow
// cycle round-trips instead of ratcheting x/y toward the narrower
// frame's clamp range.
this._ro = new ResizeObserver(() => this._render());
this._ro.observe(this);
load();
this._render();
}
disconnectedCallback() {
subs.delete(this._subFn);
this.removeEventListener('dragenter', this);
this.removeEventListener('dragover', this);
this.removeEventListener('dragleave', this);
this.removeEventListener('drop', this);
if (this._ro) { this._ro.disconnect(); this._ro = null; }
this._exitReframe(false);
}
_enterReframe() {
if (this.hasAttribute('data-reframe')) return;
this.setAttribute('data-reframe', '');
this._applyView();
// Close on click outside (the spill handler stopPropagation()s so
// in-image drags don't reach this) and on Escape. Listeners are held
// on the instance so _exitReframe / disconnectedCallback can detach
// exactly what was attached.
this._outside = (e) => {
if (e.composedPath && e.composedPath().includes(this)) return;
this._exitReframe(true);
};
this._esc = (e) => { if (e.key === 'Escape') this._exitReframe(true); };
document.addEventListener('pointerdown', this._outside, true);
document.addEventListener('keydown', this._esc, true);
}
_exitReframe(commit) {
if (!this.hasAttribute('data-reframe')) return;
if (this._dragUp) this._dragUp();
this.removeAttribute('data-reframe');
this.removeAttribute('data-panning');
if (this._outside) document.removeEventListener('pointerdown', this._outside, true);
if (this._esc) document.removeEventListener('keydown', this._esc, true);
this._outside = this._esc = null;
if (commit) this._commitView();
}
attributeChangedCallback() { if (this.shadowRoot) this._render(); }
// handleEvent — one listener object for all four drag events keeps the
// add/remove symmetric and the depth counter correct.
handleEvent(e) {
if (e.type === 'dragenter' || e.type === 'dragover') {
// Without preventDefault the browser never fires 'drop'.
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
if (e.type === 'dragenter') this._depth++;
this.setAttribute('data-over', '');
} else if (e.type === 'dragleave') {
// dragenter/leave fire for every descendant crossing — count depth
// so hovering the icon inside the empty state doesn't flicker.
if (--this._depth <= 0) { this._depth = 0; this.removeAttribute('data-over'); }
} else if (e.type === 'drop') {
e.preventDefault();
e.stopPropagation();
this._depth = 0;
this.removeAttribute('data-over');
const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
if (f) this._ingest(f);
}
}
async _ingest(file) {
this._setError(null);
if (!file || ACCEPT.indexOf(file.type) < 0) {
this._setError('Drop a PNG, JPEG, WebP, or AVIF image.');
return;
}
// toDataUrl can take hundreds of ms on a large photo. A Clear or a
// newer drop during that window would be clobbered when this await
// resumes — bump + capture a generation so stale encodes bail.
const gen = ++this._gen;
try {
const w = this.clientWidth || this.offsetWidth || MAX_DIM;
const url = await toDataUrl(file, w);
if (gen !== this._gen) return;
// Only exit reframe once the new image is in hand — a rejected type
// or decode failure leaves the in-progress crop untouched.
this._exitReframe(false);
const val = { u: url, s: 1, x: 0, y: 0 };
setSlot(this.id || '', val);
// Keep a session-local copy for id-less slots so the drop still
// shows, even though it cannot persist.
if (!this.id) { this._local = val; this._render(); }
} catch (err) {
if (gen !== this._gen) return;
this._setError('Could not read that image.');
console.warn('<image-slot> ingest failed:', err);
}
}
_setError(msg) {
if (this._err) { this._err.remove(); this._err = null; }
if (!msg) return;
const d = document.createElement('div');
d.className = 'err'; d.textContent = msg;
this.shadowRoot.appendChild(d);
this._err = d;
setTimeout(() => { if (this._err === d) { d.remove(); this._err = null; } }, 3000);
}
// Reframing (pan/resize) is only meaningful for fit=cover — contain/fill
// keep the old object-fit path and double-click is a no-op.
_reframes() {
return this.hasAttribute('data-filled') &&
(this.getAttribute('fit') || 'cover') === 'cover';
}
// Cover-baseline geometry, shared by clamp/apply/resize. Null until the
// img has loaded (naturalWidth is 0 before that) or when the slot has no
// layout box — ResizeObserver fires with a 0×0 rect under display:none,
// and clamping against a degenerate 1×1 frame would silently pull the
// stored pan toward zero.
_geom() {
const iw = this._img.naturalWidth, ih = this._img.naturalHeight;
const fw = this.clientWidth, fh = this.clientHeight;
if (!iw || !ih || !fw || !fh) return null;
return { iw, ih, fw, fh, base: Math.max(fw / iw, fh / ih) };
}
_clampView() {
// Pan range on each axis is half the overflow past the frame edge.
const g = this._geom();
if (!g) return;
const mx = Math.max(0, (g.iw * g.base * this._view.s / g.fw - 1) * 50);
const my = Math.max(0, (g.ih * g.base * this._view.s / g.fh - 1) * 50);
this._view.x = Math.max(-mx, Math.min(mx, this._view.x));
this._view.y = Math.max(-my, Math.min(my, this._view.y));
}
_applyView() {
const g = this._geom();
const fit = this.getAttribute('fit') || 'cover';
if (fit !== 'cover' || !g) {
// Non-cover, or dimensions not known yet (before img load).
this._img.style.width = '100%';
this._img.style.height = '100%';
this._img.style.left = '50%';
this._img.style.top = '50%';
this._img.style.objectFit = fit;
this._img.style.objectPosition = this.getAttribute('position') || '50% 50%';
return;
}
// Cover baseline: img fills the frame on its tighter axis at s=1, so
// pan works immediately on the overflowing axis without zooming first.
// Width/height and left/top are all frame-% — depends only on the
// frame aspect ratio, so a responsive resize keeps the same crop. The
// spill layer mirrors the same box so its corners = image corners.
const k = g.base * this._view.s;
const w = (g.iw * k / g.fw * 100) + '%';
const h = (g.ih * k / g.fh * 100) + '%';
const l = (50 + this._view.x) + '%';
const t = (50 + this._view.y) + '%';
this._img.style.width = w; this._img.style.height = h;
this._img.style.left = l; this._img.style.top = t;
this._img.style.objectFit = '';
this._spill.style.width = w; this._spill.style.height = h;
this._spill.style.left = l; this._spill.style.top = t;
}
_commitView() {
const v = { s: this._view.s, x: this._view.x, y: this._view.y };
if (this._userUrl) v.u = this._userUrl;
// Framing-only (no u) persists too so an author-src slot remembers its
// crop; clearing the sidecar still falls through to src=.
if (this.id) setSlot(this.id, v);
else { this._local = v; }
}
_render() {
// Shape / mask. Presets use border-radius so the dashed ring can
// follow the rounded outline; clip-path is only applied for an
// explicit `mask` (the ring is hidden there since a rectangle
// dashed border chopped by an arbitrary polygon looks broken).
const mask = this.getAttribute('mask');
const shape = (this.getAttribute('shape') || 'rounded').toLowerCase();
let radius = '';
if (shape === 'circle') radius = '50%';
else if (shape === 'pill') radius = '9999px';
else if (shape === 'rounded') {
const n = parseFloat(this.getAttribute('radius'));
radius = (Number.isFinite(n) ? n : 12) + 'px';
}
this._frame.style.borderRadius = mask ? '' : radius;
this._frame.style.clipPath = mask || '';
this._ring.style.borderRadius = mask ? '' : radius;
this._ring.style.display = mask ? 'none' : '';
// Controls and reframe entry gate on this so share links stay read-only.
const editable = !!(window.omelette && window.omelette.writeFile);
this.toggleAttribute('data-editable', editable);
this._sub.style.display = editable ? '' : 'none';
// Content. The sidecar is also writable by the agent's write_file
// tool, so its value isn't guaranteed canvas-originated — only accept
// data:image/ URLs from it. The `src` attribute is author-controlled
// (Claude wrote it into the HTML) so it passes through unchanged.
let stored = this.id ? getSlot(this.id) : this._local;
if (stored && stored.u && !/^data:image\//i.test(stored.u)) stored = null;
const srcAttr = this.getAttribute('src') || '';
this._userUrl = (stored && stored.u) || null;
const url = this._userUrl || srcAttr;
// Don't clobber an in-flight reframe with a store-triggered re-render.
if (!this.hasAttribute('data-reframe')) {
this._view = {
s: stored && Number.isFinite(stored.s) ? clampS(stored.s) : 1,
x: stored && Number.isFinite(stored.x) ? stored.x : 0,
y: stored && Number.isFinite(stored.y) ? stored.y : 0,
};
}
this._cap.textContent = this.getAttribute('placeholder') || 'Drop an image';
// Toggle via style.display — the [hidden] attribute alone loses to
// the display:flex / display:block rules in the stylesheet above.
if (url) {
if (this._img.getAttribute('src') !== url) {
this._img.src = url;
this._ghost.src = url;
}
this._img.style.display = 'block';
this._empty.style.display = 'none';
this.setAttribute('data-filled', '');
this._clampView();
this._applyView();
} else {
this._img.style.display = 'none';
this._img.removeAttribute('src');
this._ghost.removeAttribute('src');
this._empty.style.display = 'flex';
this.removeAttribute('data-filled');
}
}
}
if (!customElements.get('image-slot')) {
customElements.define('image-slot', ImageSlot);
}
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because it is too large Load Diff
+563
View File
@@ -0,0 +1,563 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=Jost:wght@300;400;500;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;}
body{margin:0;background:#080509;}
::selection{background:rgba(201,168,106,0.28);color:#f3ead9;}
@keyframes fadeUp{from{opacity:0;transform:translateY(14px);}to{opacity:1;transform:translateY(0);}}
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(120,190,120,0.5);}70%{box-shadow:0 0 0 9px rgba(120,190,120,0);}100%{box-shadow:0 0 0 0 rgba(120,190,120,0);}}
@keyframes drift{from{background-position:0 0;}to{background-position:60px 60px;}}
a{text-decoration:none;color:inherit;}
.wg-scroll::-webkit-scrollbar{width:0;height:0;}
</style>
</helmet>
<div style="min-height:100vh;font-family:'Jost',sans-serif;color:#cabfae;background:radial-gradient(125% 80% at 50% -5%, #1c1428 0%, #100b16 34%, #0a0710 66%, #070409 100%);background-attachment:fixed;overflow-x:hidden;">
<!-- ================= NAV ================= -->
<nav style="position:sticky;top:0;z-index:50;backdrop-filter:blur(14px);background:rgba(9,6,12,0.82);border-bottom:1px solid rgba(201,168,106,0.16);">
<div style="max-width:1240px;margin:0 auto;padding:0 32px;height:66px;display:flex;align-items:center;justify-content:space-between;gap:24px;">
<div onClick="{{ goHome }}" style="display:flex;align-items:center;gap:13px;cursor:pointer;">
<div style="width:26px;height:26px;transform:rotate(45deg);border:1.5px solid #c9a86a;display:flex;align-items:center;justify-content:center;box-shadow:0 0 14px rgba(201,168,106,0.25);">
<div style="width:9px;height:9px;background:#c9a86a;"></div>
</div>
<div style="font-family:'Cinzel',serif;font-weight:600;font-size:16px;letter-spacing:0.16em;color:#e8dcc6;white-space:nowrap;">SHADOWS OVER WESTGATE</div>
</div>
<div style="display:flex;align-items:center;gap:30px;">
<div onClick="{{ goHome }}" style="position:relative;cursor:pointer;font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;" style-hover="color:#e8dcc6;">Home
<sc-if value="{{ isHome }}" hint-placeholder-val="{{ true }}"><span style="position:absolute;left:-4px;right:-4px;bottom:-21px;height:2px;background:linear-gradient(90deg,transparent,#c9a86a,transparent);"></span></sc-if>
</div>
<div onClick="{{ goNews }}" style="position:relative;cursor:pointer;font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;" style-hover="color:#e8dcc6;">News
<sc-if value="{{ isNews }}" hint-placeholder-val="{{ false }}"><span style="position:absolute;left:-4px;right:-4px;bottom:-21px;height:2px;background:linear-gradient(90deg,transparent,#c9a86a,transparent);"></span></sc-if>
</div>
<a href="https://westgate.pw/categories" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;" style-hover="color:#e8dcc6;">Forums</a>
<a href="https://westgate.pw/wiki" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;" style-hover="color:#e8dcc6;">Wiki</a>
<div onClick="{{ goGallery }}" style="position:relative;cursor:pointer;font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;" style-hover="color:#e8dcc6;">Gallery
<sc-if value="{{ isGallery }}" hint-placeholder-val="{{ false }}"><span style="position:absolute;left:-4px;right:-4px;bottom:-21px;height:2px;background:linear-gradient(90deg,transparent,#c9a86a,transparent);"></span></sc-if>
</div>
<div onClick="{{ goBlog }}" style="position:relative;cursor:pointer;font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.13em;text-transform:uppercase;color:#c4b8a6;padding:6px 0;white-space:nowrap;" style-hover="color:#e8dcc6;">Dev Blog
<sc-if value="{{ isBlog }}" hint-placeholder-val="{{ false }}"><span style="position:absolute;left:-4px;right:-4px;bottom:-21px;height:2px;background:linear-gradient(90deg,transparent,#c9a86a,transparent);"></span></sc-if>
</div>
</div>
<div style="display:flex;align-items:center;gap:14px;">
<a href="https://westgate.pw/login" style="font-family:'Inter',sans-serif;font-size:12px;letter-spacing:0.1em;text-transform:uppercase;color:#c4b8a6;white-space:nowrap;" style-hover="color:#e8dcc6;">Log In</a>
<a href="https://westgate.pw/register" style="font-family:'Inter',sans-serif;font-size:12px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:9px 18px;border-radius:3px;border:1px solid #ddc488;box-shadow:0 2px 12px rgba(168,133,74,0.3);">Register</a>
</div>
</div>
</nav>
<!-- ================= HOME ================= -->
<sc-if value="{{ isHome }}" hint-placeholder-val="{{ true }}">
<div style="animation:fadeUp .5s ease both;">
<!-- HERO -->
<section style="position:relative;height:560px;border-bottom:1px solid rgba(201,168,106,0.16);overflow:hidden;">
<div style="position:absolute;inset:0;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-hero" shape="rect" placeholder="Drop banner key art" style="width:100%;height:100%;" hint-size="100%,100%"></x-import>
</div>
<div style="position:absolute;inset:0;pointer-events:none;background:linear-gradient(180deg,rgba(10,7,14,0.55) 0%,rgba(10,7,14,0.2) 35%,rgba(10,7,14,0.78) 82%,#0a0710 100%);"></div>
<div style="position:absolute;inset:0;pointer-events:none;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:0 24px;">
<div style="font-family:'Cinzel',serif;font-size:13px;letter-spacing:0.42em;text-transform:uppercase;color:#c9a86a;margin-bottom:22px;">Neverwinter Nights · Persistent World</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:72px;line-height:1.02;letter-spacing:0.03em;color:#f1e8d6;margin:0;text-shadow:0 4px 40px rgba(0,0,0,0.7);">Welcome to Westgate</h1>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:20px;color:#cdc1b0;max-width:620px;margin:24px 0 0;line-height:1.6;text-shadow:0 2px 16px rgba(0,0,0,0.8);">An ancient port city rises out of the mists, at the edge of the Sea of Fallen Stars. Mind the shadows.</p>
<div style="display:flex;gap:16px;margin-top:38px;pointer-events:auto;">
<a href="https://westgate.pw/wiki" style="font-family:'Inter',sans-serif;font-size:13px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:15px 32px;border-radius:3px;border:1px solid #ddc488;box-shadow:0 4px 22px rgba(168,133,74,0.35);">How to Join</a>
<div onClick="{{ goNews }}" style="cursor:pointer;font-family:'Inter',sans-serif;font-size:13px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#e2d3b4;background:rgba(20,14,26,0.55);backdrop-filter:blur(6px);padding:15px 32px;border-radius:3px;border:1px solid rgba(201,168,106,0.4);">Latest News</div>
</div>
</div>
</section>
<!-- SERVER STATUS -->
<section style="max-width:1240px;margin:0 auto;padding:0 32px;">
<div style="margin-top:-44px;position:relative;z-index:10;background:linear-gradient(160deg,#1a1322,#100b18);border:1px solid rgba(201,168,106,0.2);border-radius:5px;box-shadow:0 24px 60px rgba(0,0,0,0.5);padding:26px 34px;display:flex;align-items:center;gap:40px;flex-wrap:wrap;">
<div style="display:flex;align-items:center;gap:16px;">
<div style="width:13px;height:13px;border-radius:50%;background:#7fc07f;animation:pulse 2.4s infinite;"></div>
<div>
<div style="font-family:'Cinzel',serif;font-size:18px;letter-spacing:0.06em;color:#e8dcc6;">Server Online</div>
<div style="font-family:'Inter',sans-serif;font-size:12px;letter-spacing:0.08em;color:#8a7e6d;margin-top:3px;">Alpha · Module 0.3</div>
</div>
</div>
<div style="width:1px;height:42px;background:rgba(201,168,106,0.18);"></div>
<div>
<div style="font-family:'Cinzel',serif;font-size:26px;color:#e8dcc6;">14 <span style="font-size:15px;color:#8a7e6d;">souls online</span></div>
<div style="font-family:'Inter',sans-serif;font-size:12px;letter-spacing:0.08em;color:#8a7e6d;margin-top:2px;">Peaked 32 today</div>
</div>
<div style="width:1px;height:42px;background:rgba(201,168,106,0.18);"></div>
<div style="flex:1;min-width:180px;">
<div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.16em;text-transform:uppercase;color:#c9a86a;">Direct Connect</div>
<div style="font-family:'Inter',sans-serif;font-size:15px;color:#d8cdbb;margin-top:4px;">play.westgate.pw</div>
</div>
<a href="https://westgate.pw/wiki" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:13px 26px;border-radius:3px;border:1px solid #ddc488;">Connect Guide</a>
</div>
</section>
<!-- PITCH -->
<section style="max-width:760px;margin:0 auto;padding:96px 32px 30px;text-align:center;">
<div style="display:flex;align-items:center;justify-content:center;gap:18px;margin-bottom:40px;">
<div style="height:1px;width:80px;background:linear-gradient(90deg,transparent,#c9a86a);"></div>
<div style="width:7px;height:7px;background:#c9a86a;transform:rotate(45deg);"></div>
<div style="height:1px;width:80px;background:linear-gradient(90deg,#c9a86a,transparent);"></div>
</div>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:23px;line-height:1.7;color:#ddd1bf;margin:0 0 34px;">Long ago, merchants from distant lands filled its harbors, and gold flowed as freely as wine — but time, as it does, has a way of curdling mortal splendor.</p>
<p style="font-family:'Jost',sans-serif;font-size:17px;line-height:1.95;color:#a99d8c;margin:0 0 26px;">Today its bones are old stone and salt-worn docks, and bargains are struck beneath every lantern's glow. Here, hoarded secrets, owed favour, and unspoken truths are the true measure of power — although coin certainly helps.</p>
<p style="font-family:'Jost',sans-serif;font-size:17px;line-height:1.95;color:#a99d8c;margin:0 0 26px;">Westgate is a city ever-balanced on a blade's edge, built upon decaying decadence. The masked and nameless guilds clot the veins of the city with their greed and cruelty. Noble houses cling to fading legacies as their bloodlines tangle with curses, ambition, and desperation. Old cults and buried powers hold the Undergate by its throat. Beneath it all, antediluvian things begin to stir — patient, watchful, and hungry.</p>
<blockquote style="font-family:'Cinzel',serif;font-style:italic;font-size:22px;line-height:1.5;color:#d9bd86;margin:46px auto;max-width:560px;position:relative;">
<span style="position:absolute;left:50%;top:-34px;transform:translateX(-50%);font-size:54px;color:rgba(201,168,106,0.3);font-family:'Cinzel',serif;"></span>
To walk Westgate's streets is to wager your soul in a game of chance.
</blockquote>
<p style="font-family:'Jost',sans-serif;font-size:17px;line-height:1.95;color:#a99d8c;margin:0 0 26px;">Every alley offers opportunity, yet each carries a price. While some come seeking fortune or power, others arrive hoping to disappear — and indeed they do, for in these boulevards you are lucky to merely survive. In Westgate, few leave unchanged… and fewer still leave at all.</p>
<p style="font-family:'Cinzel',serif;font-size:19px;letter-spacing:0.04em;color:#e8dcc6;margin:34px 0 0;">Welcome to Westgate. Mind the shadows.</p>
</section>
<!-- GET STARTED -->
<section style="max-width:1240px;margin:0 auto;padding:70px 32px 30px;">
<div style="text-align:center;margin-bottom:46px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c9a86a;margin-bottom:12px;">Your First Steps</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:38px;color:#ece2d2;margin:0;letter-spacing:0.03em;">Enter the City</h2>
</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:20px;">
<a href="https://westgate.pw/wiki" style="display:block;background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;padding:30px 26px;transition:all .25s;" style-hover="border:1px solid rgba(201,168,106,0.4);transform:translateY(-4px);">
<div style="width:42px;height:42px;border:1px solid rgba(201,168,106,0.4);border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:22px;"><div style="width:13px;height:13px;border:1.5px solid #c9a86a;transform:rotate(45deg);"></div></div>
<div style="font-family:'Cinzel',serif;font-size:19px;color:#e8dcc6;margin-bottom:10px;">How to Join</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#998d7c;margin:0;">Install Neverwinter Nights: EE, point it at our server, and step through the gate.</p>
</a>
<a href="https://westgate.pw/wiki" style="display:block;background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;padding:30px 26px;transition:all .25s;" style-hover="border:1px solid rgba(201,168,106,0.4);transform:translateY(-4px);">
<div style="width:42px;height:42px;border:1px solid rgba(201,168,106,0.4);border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:22px;"><div style="width:13px;height:13px;border:1.5px solid #c9a86a;"></div></div>
<div style="font-family:'Cinzel',serif;font-size:19px;color:#e8dcc6;margin-bottom:10px;">New Player Wiki</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#998d7c;margin:0;">Starter guides, character creation, and everything you need for your first night.</p>
</a>
<a href="https://westgate.pw/categories" style="display:block;background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;padding:30px 26px;transition:all .25s;" style-hover="border:1px solid rgba(201,168,106,0.4);transform:translateY(-4px);">
<div style="width:42px;height:42px;border:1px solid rgba(201,168,106,0.4);border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:22px;"><div style="width:15px;height:11px;border:1.5px solid #c9a86a;border-radius:2px;"></div></div>
<div style="font-family:'Cinzel',serif;font-size:19px;color:#e8dcc6;margin-bottom:10px;">The Setting Bible</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#998d7c;margin:0;">Lore of Westgate — its districts, guilds, noble houses, and buried histories.</p>
</a>
<a href="https://westgate.pw/categories" style="display:block;background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;padding:30px 26px;transition:all .25s;" style-hover="border:1px solid rgba(201,168,106,0.4);transform:translateY(-4px);">
<div style="width:42px;height:42px;border:1px solid rgba(201,168,106,0.4);border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:22px;"><div style="width:11px;height:11px;border-radius:50%;border:1.5px solid #c9a86a;"></div></div>
<div style="font-family:'Cinzel',serif;font-size:19px;color:#e8dcc6;margin-bottom:10px;">Join the Community</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#998d7c;margin:0;">Introduce yourself in the forums and find your place among Westgate's denizens.</p>
</a>
</div>
</section>
<!-- LATEST NEWS STRIP -->
<section style="max-width:1240px;margin:0 auto;padding:70px 32px 100px;">
<div style="display:flex;align-items:flex-end;justify-content:space-between;margin-bottom:34px;">
<div>
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c9a86a;margin-bottom:10px;">From the Watch</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:34px;color:#ece2d2;margin:0;letter-spacing:0.03em;">Latest News</h2>
</div>
<div onClick="{{ goNews }}" style="cursor:pointer;font-family:'Inter',sans-serif;font-size:12px;letter-spacing:0.12em;text-transform:uppercase;color:#c9a86a;border-bottom:1px solid rgba(201,168,106,0.4);padding-bottom:3px;">All Dispatches →</div>
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:22px;">
<div onClick="{{ goNews }}" style="cursor:pointer;background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;overflow:hidden;transition:all .25s;" style-hover="border:1px solid rgba(201,168,106,0.4);">
<div style="height:160px;background:repeating-linear-gradient(135deg,#1c1526,#1c1526 11px,#181120 11px,#181120 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(201,168,106,0.12);"><span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;color:#5e5446;text-transform:uppercase;">news image</span></div>
<div style="padding:24px;">
<div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#8a7e6d;margin-bottom:10px;">24 Jun 2026 · Announcement</div>
<div style="font-family:'Cinzel',serif;font-size:19px;line-height:1.35;color:#e8dcc6;margin-bottom:10px;">The Gates Open — Alpha Has Begun</div>
<p style="font-family:'Jost',sans-serif;font-size:14px;line-height:1.7;color:#998d7c;margin:0;">Westgate opens its harbour to its first wave of adventurers. Here is everything you need to know.</p>
</div>
</div>
<div onClick="{{ goNews }}" style="cursor:pointer;background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;overflow:hidden;transition:all .25s;" style-hover="border:1px solid rgba(201,168,106,0.4);">
<div style="height:160px;background:repeating-linear-gradient(135deg,#1c1526,#1c1526 11px,#181120 11px,#181120 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(201,168,106,0.12);"><span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;color:#5e5446;text-transform:uppercase;">news image</span></div>
<div style="padding:24px;">
<div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#8a7e6d;margin-bottom:10px;">21 Jun 2026 · Patch Note</div>
<div style="font-family:'Cinzel',serif;font-size:19px;line-height:1.35;color:#e8dcc6;margin-bottom:10px;">Patch 0.3 — The Undergate</div>
<p style="font-family:'Jost',sans-serif;font-size:14px;line-height:1.7;color:#998d7c;margin:0;">The tunnels beneath the city are now open. New areas, factions, and dangers await below.</p>
</div>
</div>
<div onClick="{{ goNews }}" style="cursor:pointer;background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;overflow:hidden;transition:all .25s;" style-hover="border:1px solid rgba(201,168,106,0.4);">
<div style="height:160px;background:repeating-linear-gradient(135deg,#1c1526,#1c1526 11px,#181120 11px,#181120 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(201,168,106,0.12);"><span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;color:#5e5446;text-transform:uppercase;">news image</span></div>
<div style="padding:24px;">
<div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#8a7e6d;margin-bottom:10px;">18 Jun 2026 · Community</div>
<div style="font-family:'Cinzel',serif;font-size:19px;line-height:1.35;color:#e8dcc6;margin-bottom:10px;">The Community Charter</div>
<p style="font-family:'Jost',sans-serif;font-size:14px;line-height:1.7;color:#998d7c;margin:0;">Our revised rules and the principles that keep Westgate's streets worth walking.</p>
</div>
</div>
</div>
</section>
</div>
</sc-if>
<!-- ================= NEWS ================= -->
<sc-if value="{{ isNews }}" hint-placeholder-val="{{ false }}">
<div style="animation:fadeUp .5s ease both;max-width:1100px;margin:0 auto;padding:72px 32px 100px;">
<div style="text-align:center;margin-bottom:56px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.3em;text-transform:uppercase;color:#c9a86a;margin-bottom:14px;">Dispatches from the Watch</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:54px;color:#f1e8d6;margin:0;letter-spacing:0.03em;">News</h1>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:18px;color:#a99d8c;margin:18px auto 0;max-width:520px;">Official server announcements, patch notes, and word from those who keep the gates.</p>
</div>
<!-- featured -->
<div style="background:linear-gradient(160deg,#1a1322,#100b18);border:1px solid rgba(201,168,106,0.2);border-radius:6px;overflow:hidden;display:grid;grid-template-columns:1.15fr 1fr;margin-bottom:46px;box-shadow:0 20px 50px rgba(0,0,0,0.4);">
<div style="position:relative;min-height:320px;background:repeating-linear-gradient(135deg,#221931,#221931 13px,#1b1428 13px,#1b1428 26px);display:flex;align-items:center;justify-content:center;">
<span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;color:#5e5446;text-transform:uppercase;">feature image</span>
</div>
<div style="padding:46px 44px;display:flex;flex-direction:column;justify-content:center;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:18px;">
<span style="font-family:'Inter',sans-serif;font-size:10.5px;letter-spacing:0.12em;text-transform:uppercase;color:#150f08;background:#c9a86a;padding:4px 11px;border-radius:2px;font-weight:600;">Announcement</span>
<span style="font-family:'Inter',sans-serif;font-size:12px;letter-spacing:0.06em;color:#8a7e6d;">24 June 2026</span>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:31px;line-height:1.2;color:#f1e8d6;margin:0 0 18px;">The Gates of Westgate Open — Alpha Has Begun</h2>
<p style="font-family:'Jost',sans-serif;font-size:16px;line-height:1.85;color:#a99d8c;margin:0 0 26px;">After long months in the shipyards, the harbour is open. Our alpha welcomes its first denizens to walk the salt-worn docks, strike bargains beneath the lanterns, and write the first chapters of Westgate's living history. Read on for what to expect, how to join, and where to begin.</p>
<div style="display:flex;align-items:center;gap:14px;">
<a href="https://westgate.pw/category/1/news" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:12px 26px;border-radius:3px;border:1px solid #ddc488;">Read Dispatch</a>
<span style="font-family:'Inter',sans-serif;font-size:12px;color:#8a7e6d;">by The Watch · 4 min read</span>
</div>
</div>
</div>
<!-- list -->
<div style="display:flex;flex-direction:column;">
<a href="https://westgate.pw/category/1/news" style="display:grid;grid-template-columns:130px 1fr auto;gap:28px;align-items:center;padding:26px 8px;border-top:1px solid rgba(201,168,106,0.12);transition:background .2s;" style-hover="background:rgba(201,168,106,0.04);">
<div style="text-align:center;"><div style="font-family:'Cinzel',serif;font-size:30px;color:#d9bd86;line-height:1;">21</div><div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#8a7e6d;margin-top:5px;">Jun 2026</div></div>
<div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:7px;"><span style="font-family:'Inter',sans-serif;font-size:10px;letter-spacing:0.12em;text-transform:uppercase;color:#9f86b8;border:1px solid rgba(159,134,184,0.4);padding:2px 8px;border-radius:2px;">Patch Note</span></div>
<div style="font-family:'Cinzel',serif;font-size:21px;color:#e8dcc6;margin-bottom:6px;">Patch 0.3 — The Undergate Expansion</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.6;color:#998d7c;margin:0;">New sub-levels, the Quivering Thumb questline, balance passes to thievery, and a reworked lantern lighting system.</p>
</div>
<div style="font-family:'Inter',sans-serif;font-size:22px;color:#6b6151;"></div>
</a>
<a href="https://westgate.pw/category/1/news" style="display:grid;grid-template-columns:130px 1fr auto;gap:28px;align-items:center;padding:26px 8px;border-top:1px solid rgba(201,168,106,0.12);transition:background .2s;" style-hover="background:rgba(201,168,106,0.04);">
<div style="text-align:center;"><div style="font-family:'Cinzel',serif;font-size:30px;color:#d9bd86;line-height:1;">18</div><div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#8a7e6d;margin-top:5px;">Jun 2026</div></div>
<div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:7px;"><span style="font-family:'Inter',sans-serif;font-size:10px;letter-spacing:0.12em;text-transform:uppercase;color:#c9a86a;border:1px solid rgba(201,168,106,0.4);padding:2px 8px;border-radius:2px;">Community</span></div>
<div style="font-family:'Cinzel',serif;font-size:21px;color:#e8dcc6;margin-bottom:6px;">The Community Charter & Rules Revision</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.6;color:#998d7c;margin:0;">An update to our code of conduct and roleplay standards. Please review before your next session.</p>
</div>
<div style="font-family:'Inter',sans-serif;font-size:22px;color:#6b6151;"></div>
</a>
<a href="https://westgate.pw/category/1/news" style="display:grid;grid-template-columns:130px 1fr auto;gap:28px;align-items:center;padding:26px 8px;border-top:1px solid rgba(201,168,106,0.12);transition:background .2s;" style-hover="background:rgba(201,168,106,0.04);">
<div style="text-align:center;"><div style="font-family:'Cinzel',serif;font-size:30px;color:#d9bd86;line-height:1;">12</div><div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#8a7e6d;margin-top:5px;">Jun 2026</div></div>
<div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:7px;"><span style="font-family:'Inter',sans-serif;font-size:10px;letter-spacing:0.12em;text-transform:uppercase;color:#150f08;background:#c9a86a;padding:2px 8px;border-radius:2px;font-weight:600;">Announcement</span></div>
<div style="font-family:'Cinzel',serif;font-size:21px;color:#e8dcc6;margin-bottom:6px;">New Player Onboarding: The Watch Welcomes You</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.6;color:#998d7c;margin:0;">A streamlined path from download to your first night in the city — mentors now available on arrival.</p>
</div>
<div style="font-family:'Inter',sans-serif;font-size:22px;color:#6b6151;"></div>
</a>
<a href="https://westgate.pw/category/1/news" style="display:grid;grid-template-columns:130px 1fr auto;gap:28px;align-items:center;padding:26px 8px;border-top:1px solid rgba(201,168,106,0.12);border-bottom:1px solid rgba(201,168,106,0.12);transition:background .2s;" style-hover="background:rgba(201,168,106,0.04);">
<div style="text-align:center;"><div style="font-family:'Cinzel',serif;font-size:30px;color:#d9bd86;line-height:1;">06</div><div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:#8a7e6d;margin-top:5px;">Jun 2026</div></div>
<div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:7px;"><span style="font-family:'Inter',sans-serif;font-size:10px;letter-spacing:0.12em;text-transform:uppercase;color:#9f86b8;border:1px solid rgba(159,134,184,0.4);padding:2px 8px;border-radius:2px;">Maintenance</span></div>
<div style="font-family:'Cinzel',serif;font-size:21px;color:#e8dcc6;margin-bottom:6px;">Scheduled Module Reset — June 28</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.6;color:#998d7c;margin:0;">The server will be briefly unavailable for a planned reset. Expect roughly thirty minutes of downtime.</p>
</div>
<div style="font-family:'Inter',sans-serif;font-size:22px;color:#6b6151;"></div>
</a>
</div>
</div>
</sc-if>
<!-- ================= DEV BLOG ================= -->
<sc-if value="{{ isBlog }}" hint-placeholder-val="{{ false }}">
<div style="animation:fadeUp .5s ease both;max-width:880px;margin:0 auto;padding:72px 32px 100px;">
<div style="text-align:center;margin-bottom:60px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.3em;text-transform:uppercase;color:#c9a86a;margin-bottom:14px;">From the Workshop</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:54px;color:#f1e8d6;margin:0;letter-spacing:0.03em;">Developer Blog</h1>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:18px;color:#a99d8c;margin:18px auto 0;max-width:540px;">Behind-the-scenes development updates — the craft, the decisions, and the long work of building a living city.</p>
</div>
<div style="position:relative;padding-left:38px;">
<div style="position:absolute;left:8px;top:8px;bottom:8px;width:1px;background:linear-gradient(180deg,rgba(201,168,106,0.5),rgba(201,168,106,0.08));"></div>
<article style="position:relative;margin-bottom:48px;">
<div style="position:absolute;left:-34px;top:6px;width:13px;height:13px;border:1.5px solid #c9a86a;background:#100b18;transform:rotate(45deg);"></div>
<div style="background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.16);border-radius:6px;overflow:hidden;">
<div style="height:200px;background:repeating-linear-gradient(135deg,#221931,#221931 13px,#1b1428 13px,#1b1428 26px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(201,168,106,0.12);"><span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;color:#5e5446;text-transform:uppercase;">screenshot / wip render</span></div>
<div style="padding:34px 38px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
<span style="font-family:'Inter',sans-serif;font-size:10.5px;letter-spacing:0.1em;text-transform:uppercase;color:#9f86b8;border:1px solid rgba(159,134,184,0.4);padding:3px 10px;border-radius:2px;">Worldbuilding</span>
<span style="font-family:'Inter',sans-serif;font-size:12px;color:#8a7e6d;">22 June 2026 · 7 min read</span>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:27px;line-height:1.25;color:#f1e8d6;margin:0 0 16px;">Building the Undergate: Tunnels Beneath the City</h2>
<p style="font-family:'Jost',sans-serif;font-size:16px;line-height:1.9;color:#a99d8c;margin:0 0 22px;">Westgate has always been a city of two halves — the lantern-lit streets above, and the flooded warrens below. This month we walk you through how the Undergate came together: the layout philosophy, the lighting passes that took far longer than we'd like to admit, and the factions that now call the dark home.</p>
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#2a2035,#1a1322);border:1px solid rgba(201,168,106,0.3);"></div>
<span style="font-family:'Inter',sans-serif;font-size:13px;color:#b3a796;">archvillainette</span>
<span style="color:#5e5446;">·</span>
<a href="https://westgate.pw/category/84/developer-blog" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.08em;text-transform:uppercase;color:#c9a86a;">Read Entry →</a>
</div>
</div>
</div>
</article>
<article style="position:relative;margin-bottom:48px;">
<div style="position:absolute;left:-34px;top:6px;width:13px;height:13px;border:1.5px solid #c9a86a;background:#100b18;transform:rotate(45deg);"></div>
<div style="background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.16);border-radius:6px;padding:30px 36px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
<span style="font-family:'Inter',sans-serif;font-size:10.5px;letter-spacing:0.1em;text-transform:uppercase;color:#c9a86a;border:1px solid rgba(201,168,106,0.4);padding:3px 10px;border-radius:2px;">Systems</span>
<span style="font-family:'Inter',sans-serif;font-size:12px;color:#8a7e6d;">9 June 2026 · 6 min read</span>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:25px;line-height:1.25;color:#f1e8d6;margin:0 0 14px;">Faction Systems & the Masked Guilds</h2>
<p style="font-family:'Jost',sans-serif;font-size:15.5px;line-height:1.85;color:#a99d8c;margin:0 0 20px;">Reputation, favour, and betrayal — how the guild standing system tracks the webs of loyalty players weave, and why we chose to let almost everything be undone with the right bribe.</p>
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#2a2035,#1a1322);border:1px solid rgba(201,168,106,0.3);"></div>
<span style="font-family:'Inter',sans-serif;font-size:13px;color:#b3a796;">The Westgate Team</span>
<span style="color:#5e5446;">·</span>
<a href="https://westgate.pw/category/84/developer-blog" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.08em;text-transform:uppercase;color:#c9a86a;">Read Entry →</a>
</div>
</div>
</article>
<article style="position:relative;margin-bottom:48px;">
<div style="position:absolute;left:-34px;top:6px;width:13px;height:13px;border:1.5px solid #c9a86a;background:#100b18;transform:rotate(45deg);"></div>
<div style="background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.16);border-radius:6px;padding:30px 36px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
<span style="font-family:'Inter',sans-serif;font-size:10.5px;letter-spacing:0.1em;text-transform:uppercase;color:#9f86b8;border:1px solid rgba(159,134,184,0.4);padding:3px 10px;border-radius:2px;">Art & Atmosphere</span>
<span style="font-family:'Inter',sans-serif;font-size:12px;color:#8a7e6d;">27 May 2026 · 5 min read</span>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:25px;line-height:1.25;color:#f1e8d6;margin:0 0 14px;">Lighting Westgate: A Study in Lanternlight</h2>
<p style="font-family:'Jost',sans-serif;font-size:15.5px;line-height:1.85;color:#a99d8c;margin:0 0 20px;">Mood is everything in a city built on shadow. A look at our approach to colour temperature, fog, and the hundreds of placed light sources that make the docks feel cold and the taverns feel like refuge.</p>
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#2a2035,#1a1322);border:1px solid rgba(201,168,106,0.3);"></div>
<span style="font-family:'Inter',sans-serif;font-size:13px;color:#b3a796;">archvillainette</span>
<span style="color:#5e5446;">·</span>
<a href="https://westgate.pw/category/84/developer-blog" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.08em;text-transform:uppercase;color:#c9a86a;">Read Entry →</a>
</div>
</div>
</article>
<article style="position:relative;">
<div style="position:absolute;left:-34px;top:6px;width:13px;height:13px;border:1.5px solid #c9a86a;background:#100b18;transform:rotate(45deg);"></div>
<div style="background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.16);border-radius:6px;padding:30px 36px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
<span style="font-family:'Inter',sans-serif;font-size:10.5px;letter-spacing:0.1em;text-transform:uppercase;color:#c9a86a;border:1px solid rgba(201,168,106,0.4);padding:3px 10px;border-radius:2px;">Design</span>
<span style="font-family:'Inter',sans-serif;font-size:12px;color:#8a7e6d;">14 May 2026 · 8 min read</span>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:25px;line-height:1.25;color:#f1e8d6;margin:0 0 14px;">Why We Chose Persistent Roleplay</h2>
<p style="font-family:'Jost',sans-serif;font-size:15.5px;line-height:1.85;color:#a99d8c;margin:0 0 20px;">The first entry. Who we are, what Shadows Over Westgate is trying to be, and the kind of stories we hope you'll tell in its streets.</p>
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#2a2035,#1a1322);border:1px solid rgba(201,168,106,0.3);"></div>
<span style="font-family:'Inter',sans-serif;font-size:13px;color:#b3a796;">The Westgate Team</span>
<span style="color:#5e5446;">·</span>
<a href="https://westgate.pw/category/84/developer-blog" style="font-family:'Inter',sans-serif;font-size:12.5px;letter-spacing:0.08em;text-transform:uppercase;color:#c9a86a;">Read Entry →</a>
</div>
</div>
</article>
</div>
</div>
</sc-if>
<!-- ================= GALLERY ================= -->
<sc-if value="{{ isGallery }}" hint-placeholder-val="{{ false }}">
<div style="animation:fadeUp .5s ease both;max-width:1240px;margin:0 auto;padding:72px 32px 100px;">
<div style="text-align:center;margin-bottom:56px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.3em;text-transform:uppercase;color:#c9a86a;margin-bottom:14px;">The Archive</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:54px;color:#f1e8d6;margin:0;letter-spacing:0.03em;">Gallery</h1>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:18px;color:#a99d8c;margin:18px auto 0;max-width:540px;">Scenes from the streets of Westgate, captured by its denizens. Drag your own screenshots onto any frame.</p>
</div>
<div style="column-count:3;column-gap:18px;">
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g1" shape="rect" placeholder="Drop screenshot" style="width:100%;height:300px;display:block;" hint-size="100%,300px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">The Undergate at Low Tide</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@duskwarden</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g2" shape="rect" placeholder="Drop screenshot" style="width:100%;height:220px;display:block;" hint-size="100%,220px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">Rain over the Harbour</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@archvillainette</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g3" shape="rect" placeholder="Drop screenshot" style="width:100%;height:340px;display:block;" hint-size="100%,340px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">Noble Row, House Malforten</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@velvethand</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g4" shape="rect" placeholder="Drop screenshot" style="width:100%;height:240px;display:block;" hint-size="100%,240px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">Masks of the Quivering Thumb</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@silktongue</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g5" shape="rect" placeholder="Drop screenshot" style="width:100%;height:300px;display:block;" hint-size="100%,300px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">The Tankard's Rest</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@ghostofeltabbar</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g6" shape="rect" placeholder="Drop screenshot" style="width:100%;height:230px;display:block;" hint-size="100%,230px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">Cobbles after Curfew</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@ashen</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g7" shape="rect" placeholder="Drop screenshot" style="width:100%;height:330px;display:block;" hint-size="100%,330px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">The Sea of Fallen Stars at Dusk</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@cartographer</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g8" shape="rect" placeholder="Drop screenshot" style="width:100%;height:250px;display:block;" hint-size="100%,250px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">A Bargain Beneath the Lantern</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@nightjar</div></div>
</div>
<div style="break-inside:avoid;margin-bottom:18px;position:relative;border:1px solid rgba(201,168,106,0.16);border-radius:5px;overflow:hidden;">
<x-import component-from-global-scope="image-slot" from="./image-slot.js" id="wg-g9" shape="rect" placeholder="Drop screenshot" style="width:100%;height:290px;display:block;" hint-size="100%,290px"></x-import>
<div style="position:absolute;left:0;right:0;bottom:0;padding:16px 16px 13px;background:linear-gradient(180deg,transparent,rgba(8,5,12,0.9));pointer-events:none;"><div style="font-family:'Cinzel',serif;font-size:15px;color:#ece2d2;">Something in the Undergate</div><div style="font-family:'Inter',sans-serif;font-size:11px;color:#9a8d7d;margin-top:3px;">@nameless</div></div>
</div>
</div>
</div>
</sc-if>
<!-- ================= JOIN THE TEAM ================= -->
<sc-if value="{{ isRecruit }}" hint-placeholder-val="{{ false }}">
<div style="animation:fadeUp .5s ease both;">
<!-- HERO -->
<section style="max-width:1240px;margin:0 auto;padding:82px 32px 20px;">
<div style="display:grid;grid-template-columns:1.05fr 1fr;gap:56px;align-items:center;">
<div>
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c9a86a;margin-bottom:22px;">Persistent World · Made by Hand</div>
<h1 style="font-family:'Cinzel',serif;font-weight:700;font-size:52px;line-height:1.08;letter-spacing:0.02em;color:#f1e8d6;margin:0 0 22px;">Come build a world that's a pleasure to work on.</h1>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:18px;line-height:1.7;color:#bcb09e;margin:0 0 34px;max-width:30em;">Westgate is made by a handful of people who love this stuff — artists, writers, builders, scripters. We've kept the tools pleasant so your time goes where it matters: into the world itself. If you make things, we'd love to meet you.</p>
<div style="display:flex;align-items:center;gap:22px;flex-wrap:wrap;">
<a href="https://westgate.pw/categories" style="font-family:'Inter',sans-serif;font-size:13px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:15px 30px;border-radius:3px;border:1px solid #ddc488;box-shadow:0 4px 22px rgba(168,133,74,0.32);">Say Hello on Discord</a>
</div>
</div>
<div style="background:linear-gradient(160deg,#1a1322,#100b18);border:1px solid rgba(201,168,106,0.2);border-radius:6px;overflow:hidden;box-shadow:0 24px 60px rgba(0,0,0,0.5);">
<div style="display:flex;align-items:center;gap:9px;padding:13px 16px;border-bottom:1px solid rgba(201,168,106,0.16);">
<span style="display:flex;gap:6px;"><i style="width:9px;height:9px;border-radius:50%;background:#7fc07f;display:block;"></i><i style="width:9px;height:9px;border-radius:50%;background:#d3b573;display:block;"></i><i style="width:9px;height:9px;border-radius:50%;background:#9f86b8;display:block;"></i></span>
<span style="font-family:'Inter',monospace;font-size:12px;color:#8a7e6d;margin-left:6px;">westgate — the undergate</span>
</div>
<div style="aspect-ratio:16/11;background:repeating-linear-gradient(135deg,#1c1526,#1c1526 11px,#181120 11px,#181120 22px);display:flex;align-items:center;justify-content:center;">
<span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:#5e5446;background:#100b18;border:1px solid rgba(201,168,106,0.16);padding:7px 13px;border-radius:3px;">screenshot · a corner you'll help build</span>
</div>
</div>
</div>
</section>
<!-- PILLARS -->
<section id="wg-stack" style="max-width:1240px;margin:0 auto;padding:80px 32px 30px;scroll-margin-top:80px;">
<div style="text-align:center;margin-bottom:46px;">
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c9a86a;margin-bottom:12px;">Who We Need</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:36px;color:#ece2d2;margin:0 0 14px;letter-spacing:0.03em;">Every kind of maker has a place here</h2>
<p style="font-family:'Jost',sans-serif;font-size:16px;line-height:1.7;color:#a99d8c;margin:0 auto;max-width:32em;">Code, art, words, world — whatever you make, there's a part of Westgate waiting for it.</p>
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:22px;">
<article style="background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;overflow:hidden;display:flex;flex-direction:column;">
<div style="aspect-ratio:16/9;background:repeating-linear-gradient(135deg,#1c1526,#1c1526 11px,#181120 11px,#181120 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(201,168,106,0.12);"><span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:#5e5446;">toolset · scripts</span></div>
<div style="padding:26px 26px 28px;">
<h3 style="font-family:'Cinzel',serif;font-size:20px;color:#e8dcc6;margin:0 0 10px;">Tools that stay out of your way</h3>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#998d7c;margin:0;">For builders and scripters: a clean toolset and modern scripting on a fast core — none of NWScript's old quirks to fight. Everything's saved and versioned, so you can experiment without fear of breaking things.</p>
</div>
</article>
<article style="background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;overflow:hidden;display:flex;flex-direction:column;">
<div style="aspect-ratio:16/9;background:repeating-linear-gradient(135deg,#1c1526,#1c1526 11px,#181120 11px,#181120 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(201,168,106,0.12);"><span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:#5e5446;">models · textures · music</span></div>
<div style="padding:26px 26px 28px;">
<h3 style="font-family:'Cinzel',serif;font-size:20px;color:#e8dcc6;margin:0 0 10px;">Your craft, your stamp</h3>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#998d7c;margin:0;">For artists and asset-makers: models, textures, areas, sound. We're not after a house style to copy — we want the thing only you'd make, and we'll help get it into the game.</p>
</div>
</article>
<article style="background:linear-gradient(160deg,#17111f,#0f0a14);border:1px solid rgba(201,168,106,0.14);border-radius:5px;overflow:hidden;display:flex;flex-direction:column;">
<div style="aspect-ratio:16/9;background:repeating-linear-gradient(135deg,#1c1526,#1c1526 11px,#181120 11px,#181120 22px);display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(201,168,106,0.12);"><span style="font-family:'Inter',monospace;font-size:11px;letter-spacing:0.15em;text-transform:uppercase;color:#5e5446;">lore · quests · factions</span></div>
<div style="padding:26px 26px 28px;">
<h3 style="font-family:'Cinzel',serif;font-size:20px;color:#e8dcc6;margin:0 0 10px;">Ideas that reach players</h3>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.7;color:#998d7c;margin:0;">For writers and designers: a questline, a faction, a forgotten corner of the city. What you dream up this month is something players are living in the next.</p>
</div>
</article>
</div>
</section>
<!-- TEAM -->
<section style="max-width:1240px;margin:0 auto;padding:56px 32px 30px;">
<div style="background:linear-gradient(160deg,#1a1322,#100b18);border:1px solid rgba(201,168,106,0.2);border-radius:6px;box-shadow:0 20px 50px rgba(0,0,0,0.4);padding:46px 50px;display:grid;grid-template-columns:1.1fr 1fr;gap:50px;align-items:center;">
<div>
<div style="font-family:'Cinzel',serif;font-size:12px;letter-spacing:0.28em;text-transform:uppercase;color:#c9a86a;margin-bottom:14px;">What It's Actually Like</div>
<h2 style="font-family:'Cinzel',serif;font-weight:600;font-size:30px;line-height:1.2;color:#f1e8d6;margin:0 0 16px;">Low ego, high trust — and your work goes live.</h2>
<p style="font-family:'Jost',sans-serif;font-size:16px;line-height:1.85;color:#a99d8c;margin:0;">No gatekeeping, no hazing, no “figure it out yourself.” We give honest, kind feedback, lend a hand when it helps, and keep things moving. The thing you make this week is the thing players see this week.</p>
</div>
<div style="display:flex;flex-direction:column;gap:16px;">
<div style="display:flex;align-items:center;gap:14px;font-family:'Jost',sans-serif;font-size:15.5px;color:#cabfae;"><span style="width:7px;height:7px;background:#c9a86a;transform:rotate(45deg);flex:none;"></span>Honest, kind feedback on your work</div>
<div style="display:flex;align-items:center;gap:14px;font-family:'Jost',sans-serif;font-size:15.5px;color:#cabfae;"><span style="width:7px;height:7px;background:#c9a86a;transform:rotate(45deg);flex:none;"></span>A hand whenever you want one</div>
<div style="display:flex;align-items:center;gap:14px;font-family:'Jost',sans-serif;font-size:15.5px;color:#cabfae;"><span style="width:7px;height:7px;background:#c9a86a;transform:rotate(45deg);flex:none;"></span>What you make can be in-game within the week</div>
<div style="display:flex;align-items:center;gap:14px;font-family:'Jost',sans-serif;font-size:15.5px;color:#cabfae;"><span style="width:7px;height:7px;background:#c9a86a;transform:rotate(45deg);flex:none;"></span>Work at whatever pace fits your life</div>
</div>
</div>
</section>
<!-- CTA -->
<section style="max-width:760px;margin:0 auto;padding:70px 32px 100px;text-align:center;">
<div style="display:flex;align-items:center;justify-content:center;gap:18px;margin-bottom:40px;">
<div style="height:1px;width:80px;background:linear-gradient(90deg,transparent,#c9a86a);"></div>
<div style="width:7px;height:7px;background:#c9a86a;transform:rotate(45deg);"></div>
<div style="height:1px;width:80px;background:linear-gradient(90deg,#c9a86a,transparent);"></div>
</div>
<h2 style="font-family:'Cinzel',serif;font-weight:700;font-size:40px;color:#f1e8d6;margin:0 0 16px;letter-spacing:0.02em;">Sound like your kind of place?</h2>
<p style="font-family:'Jost',sans-serif;font-weight:300;font-size:18px;line-height:1.7;color:#bcb09e;margin:0 0 30px;">No test gauntlet, no résumé. Just come say hi and tell us what you'd love to build.</p>
<!-- ponytail: point at the real Discord invite when you have it -->
<a href="https://westgate.pw/categories" style="display:inline-block;font-family:'Inter',sans-serif;font-size:13px;letter-spacing:0.1em;text-transform:uppercase;font-weight:600;color:#150f08;background:linear-gradient(180deg,#d3b573,#a8854a);padding:16px 36px;border-radius:3px;border:1px solid #ddc488;box-shadow:0 4px 26px rgba(168,133,74,0.4);">Join the Team on Discord →</a>
<div style="margin-top:46px;padding-top:22px;border-top:1px solid rgba(201,168,106,0.12);font-family:'Inter',sans-serif;font-size:12px;letter-spacing:0.08em;color:#6d6356;">Artists · writers · builders · scripters — all welcome</div>
</section>
</div>
</sc-if>
<!-- ================= FOOTER ================= -->
<footer style="border-top:1px solid rgba(201,168,106,0.16);background:linear-gradient(180deg,rgba(13,9,18,0),rgba(8,5,12,0.6));margin-top:20px;">
<div style="max-width:1240px;margin:0 auto;padding:64px 32px 30px;display:grid;grid-template-columns:1.6fr 1fr 1fr;gap:50px;">
<div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:18px;">
<div style="width:22px;height:22px;transform:rotate(45deg);border:1.5px solid #c9a86a;display:flex;align-items:center;justify-content:center;"><div style="width:7px;height:7px;background:#c9a86a;"></div></div>
<div style="font-family:'Cinzel',serif;font-weight:600;font-size:15px;letter-spacing:0.14em;color:#e8dcc6;">SHADOWS OVER WESTGATE</div>
</div>
<p style="font-family:'Jost',sans-serif;font-size:14.5px;line-height:1.8;color:#8a7e6d;margin:0 0 18px;max-width:380px;">A Neverwinter Nights: Enhanced Edition persistent world of gothic intrigue, set in the Forgotten Realms port city of Westgate.</p>
<p style="font-family:'Cinzel',serif;font-style:italic;font-size:14px;color:#9a8262;margin:0;">Mind the shadows.</p>
</div>
<div>
<div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.16em;text-transform:uppercase;color:#c9a86a;margin-bottom:18px;">Explore</div>
<div style="display:flex;flex-direction:column;gap:12px;">
<div onClick="{{ goHome }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">Home</div>
<div onClick="{{ goNews }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">News</div>
<div onClick="{{ goGallery }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">Gallery</div>
<div onClick="{{ goBlog }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">Dev Blog</div>
<div onClick="{{ goRecruit }}" style="cursor:pointer;font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">Join the Team</div>
</div>
</div>
<div>
<div style="font-family:'Inter',sans-serif;font-size:11px;letter-spacing:0.16em;text-transform:uppercase;color:#c9a86a;margin-bottom:18px;">Community</div>
<div style="display:flex;flex-direction:column;gap:12px;">
<a href="https://westgate.pw/categories" style="font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">Forums</a>
<a href="https://westgate.pw/wiki" style="font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">Wiki</a>
<a href="https://westgate.pw/register" style="font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">Register</a>
<a href="https://westgate.pw/wiki" style="font-family:'Jost',sans-serif;font-size:14.5px;color:#a99d8c;" style-hover="color:#e8dcc6;">How to Join</a>
</div>
</div>
</div>
<div style="max-width:1240px;margin:0 auto;padding:22px 32px 40px;border-top:1px solid rgba(201,168,106,0.1);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;">
<div style="font-family:'Inter',sans-serif;font-size:12px;color:#6d6356;">© 2026 Shadows Over Westgate · A NWN:EE Persistent World</div>
<div style="font-family:'Inter',sans-serif;font-size:12px;color:#6d6356;">Forgotten Realms · Westgate · Sea of Fallen Stars</div>
</div>
</footer>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:1280,&quot;height&quot;:860},&quot;startPage&quot;:{&quot;editor&quot;:&quot;enum&quot;,&quot;options&quot;:[&quot;home&quot;,&quot;news&quot;,&quot;blog&quot;,&quot;gallery&quot;,&quot;recruit&quot;],&quot;default&quot;:&quot;home&quot;,&quot;tsType&quot;:&quot;'home'|'news'|'blog'|'gallery'|'recruit'&quot;}}">
class Component extends DCLogic {
state = { page: this.props.startPage || 'home' };
go(page) {
this.setState({ page });
if (typeof window !== 'undefined') {
try { window.scrollTo({ top: 0, behavior: 'smooth' }); } catch (e) { window.scrollTo(0, 0); }
}
}
renderVals() {
const p = this.state.page;
return {
isHome: p === 'home',
isNews: p === 'news',
isBlog: p === 'blog',
isGallery: p === 'gallery',
isRecruit: p === 'recruit',
goHome: () => this.go('home'),
goNews: () => this.go('news'),
goBlog: () => this.go('blog'),
goGallery: () => this.go('gallery'),
goRecruit: () => this.go('recruit'),
};
}
}
</script>
</body>
</html>
+643
View File
@@ -0,0 +1,643 @@
// @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design)
/* BEGIN USAGE */
/**
* <image-slot> — user-fillable image placeholder.
*
* Drop this into a deck, mockup, or page wherever you want the user to
* supply an image. You control the slot's shape and size; the user fills it
* by dragging an image file onto it (or clicking to browse). The dropped
* image persists across reloads via a .image-slots.state.json sidecar —
* same read-via-fetch / write-via-window.omelette pattern as
* design_canvas.jsx, so the filled slot shows on share links, downloaded
* zips, and PPTX export. Outside the omelette runtime the slot is read-only.
*
* The host bridge only allows sidecar writes at the project root, so the
* HTML that uses this component is assumed to live at the project root too
* (same constraint as design_canvas.jsx).
*
* Attributes:
* id Persistence key. REQUIRED for the drop to survive reload —
* every slot on the page needs a distinct id.
* shape 'rect' | 'rounded' | 'circle' | 'pill' (default 'rounded')
* 'circle' applies 50% border-radius; on a non-square slot
* that's an ellipse — set equal width and height for a true
* circle.
* radius Corner radius in px for 'rounded'. (default 12)
* mask Any CSS clip-path value. Overrides `shape` — use this for
* hexagons, blobs, arbitrary polygons.
* fit object-fit: cover | contain | fill. (default 'cover')
* With cover (the default) double-clicking the filled slot
* enters a reframe mode: the whole image spills past the mask
* (translucent outside, opaque inside), drag to reposition,
* corner-drag to scale. The crop persists alongside the image
* in the sidecar. contain/fill stay static.
* position object-position for fit=contain|fill. (default '50% 50%')
* placeholder Empty-state caption. (default 'Drop an image')
* src Optional initial/fallback image URL. A user drop overrides
* it; clearing the drop reveals src again.
*
* Size and layout come from ordinary CSS on the element — width/height
* inline or from a parent grid — so it composes with any layout.
*
* Usage:
* <image-slot id="hero" style="width:800px;height:450px" shape="rounded" radius="20"
* placeholder="Drop a hero image"></image-slot>
* <image-slot id="avatar" style="width:120px;height:120px" shape="circle"></image-slot>
* <image-slot id="kite" style="width:300px;height:300px"
* mask="polygon(50% 0, 100% 50%, 50% 100%, 0 50%)"></image-slot>
*/
/* END USAGE */
(() => {
const STATE_FILE = '.image-slots.state.json';
// 2× a ~600px slot in a 1920-wide deck — retina-sharp without making the
// sidecar enormous. A 1200px WebP at q=0.85 is ~150-300KB.
const MAX_DIM = 1200;
// Raster formats only. SVG is excluded (can carry script; createImageBitmap
// on SVG blobs is inconsistent). GIF is excluded because the canvas
// re-encode keeps only the first frame, so an animated GIF would silently
// go still — better to reject than surprise.
const ACCEPT = ['image/png', 'image/jpeg', 'image/webp', 'image/avif'];
// ── Shared sidecar store ────────────────────────────────────────────────
// One fetch + immediate write-on-change for every <image-slot> on the
// page. Reads via fetch() so viewing works anywhere the HTML and sidecar
// are served together; writes go through window.omelette.writeFile, which
// the host allowlists to *.state.json basenames only.
const subs = new Set();
let slots = {};
// ids explicitly cleared before the sidecar fetch resolved — otherwise
// the merge below can't tell "never set" from "just deleted" and would
// resurrect the sidecar's stale value.
const tombstones = new Set();
let loaded = false;
let loadP = null;
function load() {
if (loadP) return loadP;
loadP = fetch(STATE_FILE)
.then((r) => (r.ok ? r.json() : null))
.then((j) => {
// Merge: sidecar loses to any in-memory change that raced ahead of
// the fetch (drop or clear) so neither is clobbered by hydration.
if (j && typeof j === 'object') {
const merged = Object.assign({}, j, slots);
// A framing-only write that raced ahead of hydration must not
// drop a user image that's only on disk — inherit u from the
// sidecar for any in-memory entry that lacks one.
for (const k in slots) {
if (merged[k] && !merged[k].u && j[k]) {
merged[k].u = typeof j[k] === 'string' ? j[k] : j[k].u;
}
}
for (const id of tombstones) delete merged[id];
slots = merged;
}
tombstones.clear();
})
.catch(() => {})
.then(() => { loaded = true; subs.forEach((fn) => fn()); });
return loadP;
}
// Serialize writes so two near-simultaneous drops on different slots
// can't reorder at the backend and leave the sidecar with only the
// first. A save requested mid-flight just marks dirty and re-fires on
// completion with the then-current slots.
let saving = false;
let saveDirty = false;
function save() {
if (saving) { saveDirty = true; return; }
const w = window.omelette && window.omelette.writeFile;
if (!w) return;
saving = true;
Promise.resolve(w(STATE_FILE, JSON.stringify(slots)))
.catch(() => {})
.then(() => { saving = false; if (saveDirty) { saveDirty = false; save(); } });
}
const S_MAX = 5;
const clampS = (s) => Math.max(1, Math.min(S_MAX, s));
// Normalize a stored slot value. Pre-reframe sidecars stored a bare
// data-URL string; newer ones store {u, s, x, y}. Either shape is valid.
function getSlot(id) {
const v = slots[id];
if (!v) return null;
return typeof v === 'string' ? { u: v, s: 1, x: 0, y: 0 } : v;
}
function setSlot(id, val) {
if (!id) return;
if (val) { slots[id] = val; tombstones.delete(id); }
else { delete slots[id]; if (!loaded) tombstones.add(id); }
subs.forEach((fn) => fn());
// A drop is rare + high-value — write immediately so nav-away can't lose
// it. Gate on the initial read so we don't overwrite a sidecar we haven't
// merged yet; the merge in load() keeps this change once the read lands.
if (loaded) save(); else load().then(save);
}
// ── Image downscale ─────────────────────────────────────────────────────
// Encode through a canvas so the sidecar carries resized bytes, not the
// raw upload. Longest side is capped at 2× the slot's rendered width
// (retina) and at MAX_DIM. WebP keeps alpha and is ~10× smaller than PNG
// for photos, so there's no need for per-image format picking.
async function toDataUrl(file, targetW) {
const bitmap = await createImageBitmap(file);
try {
const cap = Math.min(MAX_DIM, Math.max(1, Math.round(targetW * 2)) || MAX_DIM);
const scale = Math.min(1, cap / Math.max(bitmap.width, bitmap.height));
const w = Math.max(1, Math.round(bitmap.width * scale));
const h = Math.max(1, Math.round(bitmap.height * scale));
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
canvas.getContext('2d').drawImage(bitmap, 0, 0, w, h);
return canvas.toDataURL('image/webp', 0.85);
} finally {
bitmap.close && bitmap.close();
}
}
// ── Custom element ──────────────────────────────────────────────────────
const stylesheet =
':host{display:inline-block;position:relative;vertical-align:top;' +
' font:13px/1.3 system-ui,-apple-system,sans-serif;color:rgba(0,0,0,.55);width:240px;height:160px}' +
'.frame{position:absolute;inset:0;overflow:hidden;background:rgba(0,0,0,.04)}' +
// .frame img (clipped) and .spill (unclipped ghost + handles) share the
// same left/top/width/height in frame-%, computed by _applyView(), so the
// inside-mask crop and the outside-mask spill stay pixel-aligned.
'.frame img{position:absolute;max-width:none;transform:translate(-50%,-50%);' +
' -webkit-user-drag:none;user-select:none;touch-action:none}' +
// Reframe mode (double-click): the full image spills past the mask. The
// spill layer is sized to the IMAGE bounds so its corners are where the
// resize handles belong. The ghost <img> inside is translucent; the real
// clipped <img> underneath shows the opaque in-mask crop.
'.spill{position:absolute;transform:translate(-50%,-50%);display:none;z-index:1;' +
' cursor:grab;touch-action:none}' +
':host([data-panning]) .spill{cursor:grabbing}' +
'.spill .ghost{position:absolute;inset:0;width:100%;height:100%;opacity:.35;' +
' pointer-events:none;-webkit-user-drag:none;user-select:none;' +
' box-shadow:0 0 0 1px rgba(0,0,0,.2),0 12px 32px rgba(0,0,0,.2)}' +
'.spill .handle{position:absolute;width:12px;height:12px;border-radius:50%;' +
' background:#fff;box-shadow:0 0 0 1.5px #c96442,0 1px 3px rgba(0,0,0,.3);' +
' transform:translate(-50%,-50%)}' +
'.spill .handle[data-c=nw]{left:0;top:0;cursor:nwse-resize}' +
'.spill .handle[data-c=ne]{left:100%;top:0;cursor:nesw-resize}' +
'.spill .handle[data-c=sw]{left:0;top:100%;cursor:nesw-resize}' +
'.spill .handle[data-c=se]{left:100%;top:100%;cursor:nwse-resize}' +
':host([data-reframe]){z-index:10}' +
':host([data-reframe]) .spill{display:block}' +
':host([data-reframe]) .frame{box-shadow:0 0 0 2px #c96442}' +
'.empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;' +
' justify-content:center;gap:6px;text-align:center;padding:12px;box-sizing:border-box;' +
' cursor:pointer;user-select:none}' +
'.empty svg{opacity:.45}' +
'.empty .cap{max-width:90%;font-weight:500;letter-spacing:.01em}' +
'.empty .sub{font-size:11px}' +
'.empty .sub u{text-underline-offset:2px;text-decoration-color:rgba(0,0,0,.25)}' +
'.empty:hover .sub u{color:rgba(0,0,0,.75);text-decoration-color:currentColor}' +
':host([data-over]) .frame{outline:2px solid #c96442;outline-offset:-2px;' +
' background:rgba(201,100,66,.10)}' +
'.ring{position:absolute;inset:0;pointer-events:none;border:1.5px dashed rgba(0,0,0,.25);' +
' transition:border-color .12s}' +
':host([data-over]) .ring{border-color:#c96442}' +
':host([data-filled]) .ring{display:none}' +
// Controls sit BELOW the mask (top:100%), absolutely positioned so the
// author-declared slot height is unaffected. The gap is padding, not a
// top offset, so the hover target stays contiguous with the frame.
'.ctl{position:absolute;top:100%;left:50%;transform:translateX(-50%);padding-top:8px;' +
' display:flex;gap:6px;opacity:0;pointer-events:none;transition:opacity .12s;z-index:2;' +
' white-space:nowrap}' +
':host([data-filled][data-editable]:hover) .ctl,:host([data-reframe]) .ctl' +
' {opacity:1;pointer-events:auto}' +
'.ctl button{appearance:none;border:0;border-radius:6px;padding:5px 10px;cursor:pointer;' +
' background:rgba(0,0,0,.65);color:#fff;font:11px/1 system-ui,-apple-system,sans-serif;' +
' backdrop-filter:blur(6px)}' +
'.ctl button:hover{background:rgba(0,0,0,.8)}' +
'.err{position:absolute;left:8px;bottom:8px;right:8px;color:#b3261e;font-size:11px;' +
' background:rgba(255,255,255,.85);padding:4px 6px;border-radius:5px;pointer-events:none}';
const icon =
'<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" ' +
'stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">' +
'<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/>' +
'<path d="m21 15-5-5L5 21"/></svg>';
class ImageSlot extends HTMLElement {
static get observedAttributes() {
return ['shape', 'radius', 'mask', 'fit', 'position', 'placeholder', 'src', 'id'];
}
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
// .spill and .ctl sit OUTSIDE .frame so overflow:hidden + border-radius
// on the frame (circle, pill, rounded) can't clip them.
root.innerHTML =
'<style>' + stylesheet + '</style>' +
'<div class="frame" part="frame">' +
' <img part="image" alt="" draggable="false" style="display:none">' +
' <div class="empty" part="empty">' + icon +
' <div class="cap"></div>' +
' <div class="sub">or <u>browse files</u></div></div>' +
' <div class="ring" part="ring"></div>' +
'</div>' +
'<div class="spill">' +
' <img class="ghost" alt="" draggable="false">' +
' <div class="handle" data-c="nw"></div><div class="handle" data-c="ne"></div>' +
' <div class="handle" data-c="sw"></div><div class="handle" data-c="se"></div>' +
'</div>' +
'<div class="ctl"><button data-act="replace" title="Replace image">Replace</button>' +
' <button data-act="clear" title="Remove image">Remove</button></div>' +
'<input type="file" accept="' + ACCEPT.join(',') + '" hidden>';
this._frame = root.querySelector('.frame');
this._ring = root.querySelector('.ring');
this._img = root.querySelector('.frame img');
this._empty = root.querySelector('.empty');
this._cap = root.querySelector('.cap');
this._sub = root.querySelector('.sub');
this._spill = root.querySelector('.spill');
this._ghost = root.querySelector('.ghost');
this._err = null;
this._input = root.querySelector('input');
this._depth = 0;
this._gen = 0;
this._view = { s: 1, x: 0, y: 0 };
this._subFn = () => this._render();
// Shadow-DOM listeners live with the shadow DOM — bound once here so
// disconnect/reconnect (e.g. React remount) doesn't stack handlers.
this._empty.addEventListener('click', () => this._input.click());
root.addEventListener('click', (e) => {
const act = e.target && e.target.getAttribute && e.target.getAttribute('data-act');
if (act === 'replace') { this._exitReframe(true); this._input.click(); }
if (act === 'clear') {
this._exitReframe(false);
this._gen++;
this._local = null;
if (this.id) setSlot(this.id, null); else this._render();
}
});
this._input.addEventListener('change', () => {
const f = this._input.files && this._input.files[0];
if (f) this._ingest(f);
this._input.value = '';
});
// naturalWidth/Height aren't known until load — re-apply so the cover
// baseline is computed from real dimensions, not the 100%×100% fallback.
this._img.addEventListener('load', () => this._applyView());
// Gated on editable + fit=cover so share links and contain/fill slots
// stay static.
this.addEventListener('dblclick', (e) => {
if (!this.hasAttribute('data-editable') || !this._reframes()) return;
e.preventDefault();
if (this.hasAttribute('data-reframe')) this._exitReframe(true);
else this._enterReframe();
});
// Pan + resize both originate on the spill layer. A handle pointerdown
// drives an aspect-locked resize anchored at the opposite corner; any
// other pointerdown on the spill pans. Offsets are frame-% so a
// reframed slot survives responsive resize / PPTX export.
this._spill.addEventListener('pointerdown', (e) => {
if (e.button !== 0 || !this.hasAttribute('data-reframe')) return;
e.preventDefault();
e.stopPropagation();
this._spill.setPointerCapture(e.pointerId);
const rect = this.getBoundingClientRect();
const fw = rect.width || 1, fh = rect.height || 1;
const corner = e.target.getAttribute && e.target.getAttribute('data-c');
let move;
if (corner) {
// Resize about the OPPOSITE corner. Viewport-px throughout (rect
// fw/fh, not clientWidth) so the math survives a transform:scale()
// ancestor — deck_stage renders slides scaled-to-fit.
const iw = this._img.naturalWidth || 1, ih = this._img.naturalHeight || 1;
const base = Math.max(fw / iw, fh / ih);
const sx = corner.includes('e') ? 1 : -1;
const sy = corner.includes('s') ? 1 : -1;
const s0 = this._view.s;
const w0 = iw * base * s0, h0 = ih * base * s0;
const cx0 = (50 + this._view.x) / 100 * fw;
const cy0 = (50 + this._view.y) / 100 * fh;
const ox = cx0 - sx * w0 / 2, oy = cy0 - sy * h0 / 2;
const diag0 = Math.hypot(w0, h0);
const ux = sx * w0 / diag0, uy = sy * h0 / diag0;
move = (ev) => {
const proj = (ev.clientX - rect.left - ox) * ux +
(ev.clientY - rect.top - oy) * uy;
const s = clampS(s0 * proj / diag0);
const d = diag0 * s / s0;
this._view.s = s;
this._view.x = (ox + ux * d / 2) / fw * 100 - 50;
this._view.y = (oy + uy * d / 2) / fh * 100 - 50;
this._clampView();
this._applyView();
};
} else {
this.setAttribute('data-panning', '');
const start = { px: e.clientX, py: e.clientY, x: this._view.x, y: this._view.y };
move = (ev) => {
this._view.x = start.x + (ev.clientX - start.px) / fw * 100;
this._view.y = start.y + (ev.clientY - start.py) / fh * 100;
this._clampView();
this._applyView();
};
}
const up = () => {
try { this._spill.releasePointerCapture(e.pointerId); } catch {}
this._spill.removeEventListener('pointermove', move);
this._spill.removeEventListener('pointerup', up);
this._spill.removeEventListener('pointercancel', up);
this.removeAttribute('data-panning');
this._dragUp = null;
};
// Stashed so _exitReframe (Escape / outside-click mid-drag) can
// tear the capture + listeners down synchronously.
this._dragUp = up;
this._spill.addEventListener('pointermove', move);
this._spill.addEventListener('pointerup', up);
this._spill.addEventListener('pointercancel', up);
});
// Wheel zoom stays available inside reframe mode as a trackpad nicety —
// zooms toward the cursor (offset' = cursor·(1-k) + offset·k).
this.addEventListener('wheel', (e) => {
if (!this.hasAttribute('data-reframe')) return;
e.preventDefault();
const r = this.getBoundingClientRect();
const cx = (e.clientX - r.left) / r.width * 100 - 50;
const cy = (e.clientY - r.top) / r.height * 100 - 50;
const prev = this._view.s;
const next = clampS(prev * Math.pow(1.0015, -e.deltaY));
if (next === prev) return;
const k = next / prev;
this._view.s = next;
this._view.x = cx * (1 - k) + this._view.x * k;
this._view.y = cy * (1 - k) + this._view.y * k;
this._clampView();
this._applyView();
}, { passive: false });
}
connectedCallback() {
// Warn once per page — an id-less slot works for the session but
// cannot persist, and two id-less slots would share nothing.
if (!this.id && !ImageSlot._warned) {
ImageSlot._warned = true;
console.warn('<image-slot> without an id will not persist its dropped image.');
}
this.addEventListener('dragenter', this);
this.addEventListener('dragover', this);
this.addEventListener('dragleave', this);
this.addEventListener('drop', this);
subs.add(this._subFn);
// width%/height% in _applyView encode the frame aspect at call time —
// a host resize (responsive grid, pane divider) would stretch the
// image until the next _render. Re-render on size change: _render()
// re-seeds _view from stored before clamp/apply, so a shrink→grow
// cycle round-trips instead of ratcheting x/y toward the narrower
// frame's clamp range.
this._ro = new ResizeObserver(() => this._render());
this._ro.observe(this);
load();
this._render();
}
disconnectedCallback() {
subs.delete(this._subFn);
this.removeEventListener('dragenter', this);
this.removeEventListener('dragover', this);
this.removeEventListener('dragleave', this);
this.removeEventListener('drop', this);
if (this._ro) { this._ro.disconnect(); this._ro = null; }
this._exitReframe(false);
}
_enterReframe() {
if (this.hasAttribute('data-reframe')) return;
this.setAttribute('data-reframe', '');
this._applyView();
// Close on click outside (the spill handler stopPropagation()s so
// in-image drags don't reach this) and on Escape. Listeners are held
// on the instance so _exitReframe / disconnectedCallback can detach
// exactly what was attached.
this._outside = (e) => {
if (e.composedPath && e.composedPath().includes(this)) return;
this._exitReframe(true);
};
this._esc = (e) => { if (e.key === 'Escape') this._exitReframe(true); };
document.addEventListener('pointerdown', this._outside, true);
document.addEventListener('keydown', this._esc, true);
}
_exitReframe(commit) {
if (!this.hasAttribute('data-reframe')) return;
if (this._dragUp) this._dragUp();
this.removeAttribute('data-reframe');
this.removeAttribute('data-panning');
if (this._outside) document.removeEventListener('pointerdown', this._outside, true);
if (this._esc) document.removeEventListener('keydown', this._esc, true);
this._outside = this._esc = null;
if (commit) this._commitView();
}
attributeChangedCallback() { if (this.shadowRoot) this._render(); }
// handleEvent — one listener object for all four drag events keeps the
// add/remove symmetric and the depth counter correct.
handleEvent(e) {
if (e.type === 'dragenter' || e.type === 'dragover') {
// Without preventDefault the browser never fires 'drop'.
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
if (e.type === 'dragenter') this._depth++;
this.setAttribute('data-over', '');
} else if (e.type === 'dragleave') {
// dragenter/leave fire for every descendant crossing — count depth
// so hovering the icon inside the empty state doesn't flicker.
if (--this._depth <= 0) { this._depth = 0; this.removeAttribute('data-over'); }
} else if (e.type === 'drop') {
e.preventDefault();
e.stopPropagation();
this._depth = 0;
this.removeAttribute('data-over');
const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
if (f) this._ingest(f);
}
}
async _ingest(file) {
this._setError(null);
if (!file || ACCEPT.indexOf(file.type) < 0) {
this._setError('Drop a PNG, JPEG, WebP, or AVIF image.');
return;
}
// toDataUrl can take hundreds of ms on a large photo. A Clear or a
// newer drop during that window would be clobbered when this await
// resumes — bump + capture a generation so stale encodes bail.
const gen = ++this._gen;
try {
const w = this.clientWidth || this.offsetWidth || MAX_DIM;
const url = await toDataUrl(file, w);
if (gen !== this._gen) return;
// Only exit reframe once the new image is in hand — a rejected type
// or decode failure leaves the in-progress crop untouched.
this._exitReframe(false);
const val = { u: url, s: 1, x: 0, y: 0 };
setSlot(this.id || '', val);
// Keep a session-local copy for id-less slots so the drop still
// shows, even though it cannot persist.
if (!this.id) { this._local = val; this._render(); }
} catch (err) {
if (gen !== this._gen) return;
this._setError('Could not read that image.');
console.warn('<image-slot> ingest failed:', err);
}
}
_setError(msg) {
if (this._err) { this._err.remove(); this._err = null; }
if (!msg) return;
const d = document.createElement('div');
d.className = 'err'; d.textContent = msg;
this.shadowRoot.appendChild(d);
this._err = d;
setTimeout(() => { if (this._err === d) { d.remove(); this._err = null; } }, 3000);
}
// Reframing (pan/resize) is only meaningful for fit=cover — contain/fill
// keep the old object-fit path and double-click is a no-op.
_reframes() {
return this.hasAttribute('data-filled') &&
(this.getAttribute('fit') || 'cover') === 'cover';
}
// Cover-baseline geometry, shared by clamp/apply/resize. Null until the
// img has loaded (naturalWidth is 0 before that) or when the slot has no
// layout box — ResizeObserver fires with a 0×0 rect under display:none,
// and clamping against a degenerate 1×1 frame would silently pull the
// stored pan toward zero.
_geom() {
const iw = this._img.naturalWidth, ih = this._img.naturalHeight;
const fw = this.clientWidth, fh = this.clientHeight;
if (!iw || !ih || !fw || !fh) return null;
return { iw, ih, fw, fh, base: Math.max(fw / iw, fh / ih) };
}
_clampView() {
// Pan range on each axis is half the overflow past the frame edge.
const g = this._geom();
if (!g) return;
const mx = Math.max(0, (g.iw * g.base * this._view.s / g.fw - 1) * 50);
const my = Math.max(0, (g.ih * g.base * this._view.s / g.fh - 1) * 50);
this._view.x = Math.max(-mx, Math.min(mx, this._view.x));
this._view.y = Math.max(-my, Math.min(my, this._view.y));
}
_applyView() {
const g = this._geom();
const fit = this.getAttribute('fit') || 'cover';
if (fit !== 'cover' || !g) {
// Non-cover, or dimensions not known yet (before img load).
this._img.style.width = '100%';
this._img.style.height = '100%';
this._img.style.left = '50%';
this._img.style.top = '50%';
this._img.style.objectFit = fit;
this._img.style.objectPosition = this.getAttribute('position') || '50% 50%';
return;
}
// Cover baseline: img fills the frame on its tighter axis at s=1, so
// pan works immediately on the overflowing axis without zooming first.
// Width/height and left/top are all frame-% — depends only on the
// frame aspect ratio, so a responsive resize keeps the same crop. The
// spill layer mirrors the same box so its corners = image corners.
const k = g.base * this._view.s;
const w = (g.iw * k / g.fw * 100) + '%';
const h = (g.ih * k / g.fh * 100) + '%';
const l = (50 + this._view.x) + '%';
const t = (50 + this._view.y) + '%';
this._img.style.width = w; this._img.style.height = h;
this._img.style.left = l; this._img.style.top = t;
this._img.style.objectFit = '';
this._spill.style.width = w; this._spill.style.height = h;
this._spill.style.left = l; this._spill.style.top = t;
}
_commitView() {
const v = { s: this._view.s, x: this._view.x, y: this._view.y };
if (this._userUrl) v.u = this._userUrl;
// Framing-only (no u) persists too so an author-src slot remembers its
// crop; clearing the sidecar still falls through to src=.
if (this.id) setSlot(this.id, v);
else { this._local = v; }
}
_render() {
// Shape / mask. Presets use border-radius so the dashed ring can
// follow the rounded outline; clip-path is only applied for an
// explicit `mask` (the ring is hidden there since a rectangle
// dashed border chopped by an arbitrary polygon looks broken).
const mask = this.getAttribute('mask');
const shape = (this.getAttribute('shape') || 'rounded').toLowerCase();
let radius = '';
if (shape === 'circle') radius = '50%';
else if (shape === 'pill') radius = '9999px';
else if (shape === 'rounded') {
const n = parseFloat(this.getAttribute('radius'));
radius = (Number.isFinite(n) ? n : 12) + 'px';
}
this._frame.style.borderRadius = mask ? '' : radius;
this._frame.style.clipPath = mask || '';
this._ring.style.borderRadius = mask ? '' : radius;
this._ring.style.display = mask ? 'none' : '';
// Controls and reframe entry gate on this so share links stay read-only.
const editable = !!(window.omelette && window.omelette.writeFile);
this.toggleAttribute('data-editable', editable);
this._sub.style.display = editable ? '' : 'none';
// Content. The sidecar is also writable by the agent's write_file
// tool, so its value isn't guaranteed canvas-originated — only accept
// data:image/ URLs from it. The `src` attribute is author-controlled
// (Claude wrote it into the HTML) so it passes through unchanged.
let stored = this.id ? getSlot(this.id) : this._local;
if (stored && stored.u && !/^data:image\//i.test(stored.u)) stored = null;
const srcAttr = this.getAttribute('src') || '';
this._userUrl = (stored && stored.u) || null;
const url = this._userUrl || srcAttr;
// Don't clobber an in-flight reframe with a store-triggered re-render.
if (!this.hasAttribute('data-reframe')) {
this._view = {
s: stored && Number.isFinite(stored.s) ? clampS(stored.s) : 1,
x: stored && Number.isFinite(stored.x) ? stored.x : 0,
y: stored && Number.isFinite(stored.y) ? stored.y : 0,
};
}
this._cap.textContent = this.getAttribute('placeholder') || 'Drop an image';
// Toggle via style.display — the [hidden] attribute alone loses to
// the display:flex / display:block rules in the stylesheet above.
if (url) {
if (this._img.getAttribute('src') !== url) {
this._img.src = url;
this._ghost.src = url;
}
this._img.style.display = 'block';
this._empty.style.display = 'none';
this.setAttribute('data-filled', '');
this._clampView();
this._applyView();
} else {
this._img.style.display = 'none';
this._img.removeAttribute('src');
this._ghost.removeAttribute('src');
this._empty.style.display = 'flex';
this.removeAttribute('data-filled');
}
}
}
if (!customElements.get('image-slot')) {
customElements.define('image-slot', ImageSlot);
}
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 KiB

File diff suppressed because it is too large Load Diff
@@ -0,0 +1,306 @@
# Westgate live pages — theme integration spec
This covers the parts that live in the **theme repo** (`nodebb-theme-westgate`),
not in ACP widgets:
- **News** (`/category/1/...`) and **Dev Blog** (`/category/84/...`) — rendered as
banner-led article lists + article pages, pulled live from the forum.
- **Gallery** — a player-upload screenshot category rendered as a masonry grid.
- **Top bar** — global header partial that replaces the left sidebar.
The two static pages (Home, Join the Team) are NOT here — they ship as
paste-ready HTML widgets in `westgate-pages/home.html` and
`westgate-pages/join-the-team.html`.
---
## 0. One SCSS partial for all live pages
Create `scss/westgate/_pages.scss` and add it to `theme.scss`:
```scss
// theme.scss (add after _widgets, before _responsive)
@import "./scss/westgate/pages";
```
All page styles use the existing `--wg-*` tokens — no new colours. Scope every
rule under a body/template class so forum chrome is untouched. Suggested top-level
hooks (added in the template overrides below):
- `body.wg-cat-news`, `body.wg-cat-blog` — the News / Dev Blog category index
- `body.wg-article-news`, `body.wg-article-blog` — an individual post
- `body.wg-gallery` — the Gallery category index
---
## 1. Category index re-skin (News / Dev Blog)
NodeBB renders a category at `categories/category.tpl` (we already override
`category.tpl`). Rather than fork the whole template, branch on the category id
and render a "cards" layout for cid 1 & 84, falling back to the normal list
otherwise.
### 1a. Tag the body (lib/theme.js → filterMiddlewareRenderHeader or a new hook)
Add a body class so SCSS can target these categories. In `lib/theme.js`, extend
the existing `filterMiddlewareRenderHeader` (or add `filter:category.build`):
```js
// lib/theme.js
const NEWS_CIDS = { 1: 'news', 84: 'blog' };
library.filterCategoryBuild = async function (hookData) {
const cid = hookData.templateData && hookData.templateData.cid;
if (NEWS_CIDS[cid]) {
hookData.templateData.bodyClass =
(hookData.templateData.bodyClass || '') + ' wg-cat-' + NEWS_CIDS[cid];
hookData.templateData.wgArticleList = true; // template flag
}
return hookData;
};
```
Register it in `plugin.json` hooks:
```json
{ "hook": "filter:category.build", "method": "filterCategoryBuild" }
```
(If `filter:category.build` isn't available in your NodeBB version, set the flag
in the existing `filter:middleware.renderHeader` by inspecting `req.url`.)
### 1b. Template branch (templates/category.tpl)
At the top of the topic loop, wrap the Westgate card markup behind the flag.
Each topic already exposes `title`, `slug`, `timestamp`/`timestampISO`,
`teaser`, `user`, and `thumb` (topic thumbnail) — that's everything the card needs.
```html
{{{ if wgArticleList }}}
<ul class="wg-article-list list-unstyled">
{{{ each topics }}}
<li class="wg-article-card">
<a class="wg-article-card__media" href="{config.relative_path}/topic/{./slug}">
{{{ if ./thumb }}}<img src="{./thumb}" alt="" loading="lazy">{{{ end }}}
</a>
<div class="wg-article-card__body">
<div class="wg-article-card__meta">
<span class="wg-tag">{./category.name}</span>
<time datetime="{./timestampISO}">{function.humanReadableDate, ./timestamp}</time>
</div>
<h2 class="wg-article-card__title">
<a href="{config.relative_path}/topic/{./slug}">{./title}</a>
</h2>
<p class="wg-article-card__teaser">{./teaser.content}</p>
<div class="wg-article-card__byline">
by {./user.username}
</div>
</div>
</li>
{{{ end }}}
</ul>
{{{ else }}}
<!-- existing default category list markup -->
{{{ end }}}
```
Mirror the visual target from the mockup (Westgate.dc.html → News view):
featured first card larger, the rest in a grid. The SCSS below handles that with
`:first-child`.
### 1c. SCSS (in _pages.scss)
```scss
body.wg-cat-news,
body.wg-cat-blog {
.wg-article-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 22px;
}
.wg-article-card {
background: var(--wg-velvet-panel);
border: 1px solid var(--wg-border);
border-radius: 8px;
box-shadow: var(--wg-velvet-shadow);
overflow: hidden;
transition: border-color .25s;
&:hover { border-color: rgba(194, 163, 90, .4); }
// featured first post spans the row
&:first-child {
grid-column: 1 / -1;
display: grid;
grid-template-columns: 1.15fr 1fr;
}
}
.wg-article-card__media img {
width: 100%; height: 160px; object-fit: cover; display: block;
border-bottom: 1px solid rgba(194, 163, 90, .12);
}
.wg-article-card:first-child .wg-article-card__media img {
height: 100%; min-height: 300px; border-bottom: 0;
}
.wg-article-card__body { padding: 22px 24px; }
.wg-article-card__meta {
font-family: var(--wg-font-code);
font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
color: var(--wg-text-muted);
display: flex; gap: 10px; align-items: center; margin-bottom: 10px;
}
.wg-tag {
color: #150f08; background: var(--wg-gold);
padding: 3px 9px; border-radius: 2px; font-weight: 600;
}
.wg-article-card__title {
font-family: var(--wg-font-display); font-size: 19px; line-height: 1.35;
color: var(--wg-text); margin: 0 0 10px;
a { color: inherit; }
}
.wg-article-card:first-child .wg-article-card__title { font-size: 28px; }
.wg-article-card__teaser {
font-family: var(--wg-font-text); font-size: 14px; line-height: 1.7;
color: var(--wg-text-muted); margin: 0;
}
.wg-article-card__byline {
font-family: var(--wg-font-text); font-size: 12.5px;
color: var(--wg-text-muted); margin-top: 14px;
}
}
@media (max-width: 900px) {
body.wg-cat-news .wg-article-list,
body.wg-cat-blog .wg-article-list { grid-template-columns: 1fr; }
body.wg-cat-news .wg-article-card:first-child,
body.wg-cat-blog .wg-article-card:first-child { grid-template-columns: 1fr; }
}
```
---
## 2. Article page re-skin (an individual News / Dev Blog topic)
Override `topic.tpl` (or just its main-post partial) conditionally for cid 1 & 84.
Tag the body the same way (`filter:topic.build``wg-article-news/blog`), then:
1. **Banner hero** — use `topic.thumb`, or the first image in the opening post,
as a full-width bordered banner; overlay the category tag + date + Cinzel
title + author byline. (Visual target: Westgate.dc.html → article view.)
2. **Prose body** — add the wiki's prose class to the first post's content
container so it inherits the magazine typography you already ship:
`class="... wiki-article-prose"`. That single class gives gold H2 rules,
blockquotes, drop-in image framing, etc. — no new CSS.
3. **Replies** — leave the normal topic reply list below the article untouched.
```html
{{{ if wgArticle }}}
<header class="wg-article-hero" {{{ if topic.thumb }}}style="--wg-hero-img:url('{topic.thumb}')"{{{ end }}}>
<div class="wg-article-hero__veil"></div>
<div class="wg-article-hero__in">
<div class="wg-article-hero__meta">
<span class="wg-tag">{topic.category.name}</span>
<time datetime="{topic.timestampISO}">{function.humanReadableDate, topic.timestamp}</time>
</div>
<h1 class="wg-article-hero__title">{topic.titleRaw}</h1>
<div class="wg-article-hero__byline">by {topic.author.username}</div>
</div>
</header>
{{{ end }}}
```
```scss
body.wg-article-news, body.wg-article-blog {
.wg-article-hero {
position: relative; min-height: 420px; border-radius: 10px;
border: 1px solid rgba(194, 163, 90, .22);
display: flex; align-items: flex-end; overflow: hidden;
background: var(--wg-hero-img) center/cover no-repeat, var(--wg-panel-2);
}
.wg-article-hero__veil {
position: absolute; inset: 0;
background: linear-gradient(180deg, rgba(9,8,11,.2), rgba(9,8,11,.55) 55%, rgba(9,8,11,.94));
}
.wg-article-hero__in { position: relative; padding: 46px 50px; }
.wg-article-hero__meta {
font-family: var(--wg-font-code); font-size: 12px; color: var(--wg-text-soft);
display: flex; gap: 12px; align-items: center; margin-bottom: 16px;
}
.wg-article-hero__title {
font-family: var(--wg-font-display); font-weight: 700;
font-size: clamp(34px, 4.4vw, 52px); line-height: 1.08;
color: #f3ede4; margin: 0; max-width: 18em;
text-shadow: 0 4px 30px rgba(0,0,0,.7);
}
// article body inherits .wiki-article-prose — no extra rules needed
// constrain measure:
[component="post/content"].wiki-article-prose { max-width: 760px; margin-inline: auto; }
}
```
---
## 3. Gallery (player-upload category → masonry)
Make a new forum category "Gallery" (note its cid). Players post a topic with a
screenshot; the topic thumbnail / first image is the tile. Re-skin its category
view as masonry (CSS columns), same pattern as §1:
```scss
body.wg-gallery {
.wg-gallery-grid { column-count: 3; column-gap: 18px; }
.wg-gallery-tile {
break-inside: avoid; margin-bottom: 18px; position: relative;
border: 1px solid var(--wg-border); border-radius: 5px; overflow: hidden;
img { width: 100%; display: block; }
}
.wg-gallery-tile__cap {
position: absolute; inset: auto 0 0 0; padding: 16px;
background: linear-gradient(180deg, transparent, rgba(8,7,10,.9));
.t { font-family: var(--wg-font-display); font-size: 15px; color: #ece2d2; }
.u { font-family: var(--wg-font-text); font-size: 11px; color: var(--wg-text-muted); }
}
}
@media (max-width: 900px) { body.wg-gallery .wg-gallery-grid { column-count: 2; } }
@media (max-width: 560px) { body.wg-gallery .wg-gallery-grid { column-count: 1; } }
```
Template: in `category.tpl`, add a `wgGallery` branch that loops topics into
`.wg-gallery-tile` using each topic's image + title + poster.
---
## 4. Top bar (global header partial — replaces the sidebar)
Visual spec: `Westgate Top Bar.dc.html` (member / guest / Forums menu / user menu /
mobile). Implementation outline:
1. **New partial** `templates/partials/header/topbar.tpl` containing brand +
primary nav + the right cluster (search toggle, notifications, chat, user menu).
Reuse Harmony's existing live components so they keep working — pull the
notification/chat/user dropdown includes from `nodebb-theme-harmony`'s
`partials/header/` rather than rebuilding them.
2. **Mount it** in `templates/header.tpl`: replace the
`<!-- IMPORT partials/sidebar-left.tpl -->` line with
`<!-- IMPORT partials/header/topbar.tpl -->`, and drop the `layout-container`
left-column so `#panel` is full-width.
3. **Nav items** come from ACP → Navigation: News, Gallery, Wiki, Forums, Dev Blog.
The "Forums" item is a dropdown whose panel lists categories + Recent/Unread/
Tags/All-categories (sample markup in the mockup; wire to your real category
list later).
4. **Turn off sidebars**: set `openSidebars` off (already the default) and, since
the left column is removed, the forum content goes full width. Keep the
per-template `sidebar` widget areas empty.
5. SCSS for the bar goes in `_pages.scss` (or a dedicated `_topbar.scss`), all
`--wg-*` based — lift the literals from the mockup.
---
## Build / preview
After adding `_pages.scss` and the template branches:
```
./nodebb build # or: ./nodebb dev for live rebuilds
```
Visual targets to match pixel-for-pixel live in this project:
- `Westgate.dc.html` — News index, article view, Gallery, Home, Join the Team
- `Westgate Top Bar.dc.html` — the global bar and all its states
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Westgate page preview (theme harness)</title>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=Jost:wght@300;400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
/* --- Simulated theme globals (mirror of scss/westgate/_tokens.scss :root) --- */
:root{
--wg-font-display:"Cinzel",Georgia,serif;
--wg-font-text:"Jost",system-ui,sans-serif;
--wg-font-ui:"Jost",system-ui,sans-serif;
--wg-font-code:"IBM Plex Mono",ui-monospace,monospace;
--wg-bg:#0f0d12;--wg-bg-deep:#09080b;--wg-panel:#18141d;--wg-panel-2:#110f15;
--wg-plum:#2a1222;--wg-plum-soft:#3a1830;--wg-gold:#c2a35a;--wg-gold-soft:#a8893f;
--wg-red:#8e3438;--wg-status-online:#7fb86a;
--wg-text:#e6e0d6;--wg-text-soft:#b9b2a6;--wg-text-muted:#9a9086;
--wg-border:rgba(194,163,90,.14);--wg-border-soft:rgba(194,163,90,.08);
--wg-velvet-panel:linear-gradient(100deg,rgba(42,18,34,.46) 0%,rgba(25,22,31,.95) 23%,rgba(18,16,23,.99) 62%,rgba(13,12,17,.98) 100%);
--wg-velvet-panel-hover:linear-gradient(100deg,rgba(58,24,48,.56) 0%,rgba(31,25,39,.98) 23%,rgba(21,18,27,1) 62%,rgba(15,14,20,.99) 100%);
--wg-velvet-shadow:inset 0 1px 0 rgba(255,255,255,.025),inset 0 0 34px rgba(96,32,68,.11),0 10px 26px rgba(0,0,0,.24);
}
html,body{margin:0;background:var(--wg-bg);color:var(--wg-text-soft);font-family:var(--wg-font-text);}
/* simulate Harmony brand hero banner + content container */
.harness-brand{max-width:1320px;margin:0 auto;padding:14px 20px 0;}
.harness-brand .bar{border:1px solid rgba(194,163,90,.22);border-radius:10px;background:linear-gradient(90deg,rgba(5,4,7,.82),rgba(10,8,12,0) 52%),#16121b;padding:18px 26px;display:flex;align-items:center;gap:14px;box-shadow:0 18px 42px rgba(0,0,0,.3);}
.harness-brand .logo{width:22px;height:22px;transform:rotate(45deg);border:1.5px solid var(--wg-gold);display:flex;align-items:center;justify-content:center;}
.harness-brand .logo i{width:7px;height:7px;background:var(--wg-gold);}
.harness-brand .ttl{font-family:var(--wg-font-display);font-weight:600;font-size:18px;letter-spacing:.12em;color:var(--wg-text);text-transform:uppercase;}
.harness-brand .sub{font-family:var(--wg-font-display);font-size:11px;letter-spacing:.22em;color:var(--wg-gold);text-transform:uppercase;}
.harness-nav{max-width:1320px;margin:10px auto 0;padding:0 20px;display:flex;gap:26px;}
.harness-nav a{font-family:var(--wg-font-ui);font-size:12.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--wg-text-soft);text-decoration:none;padding:8px 0;border-bottom:2px solid transparent;}
.harness-nav a.active{color:var(--wg-text);border-bottom-color:var(--wg-gold);}
.harness-content{max-width:1320px;margin:18px auto 0;padding:0 20px 60px;}
.harness-label{max-width:1320px;margin:0 auto;padding:10px 20px 0;font-family:var(--wg-font-code);font-size:11px;color:#6b6152;letter-spacing:.1em;text-transform:uppercase;}
</style>
</head>
<body>
<div class="harness-label">▾ Simulated theme chrome (brand hero + nav + #content) — the widget HTML is everything below the nav</div>
<div class="harness-brand"><div class="bar"><div class="logo"><i></i></div><div><div class="ttl">Shadows Over Westgate</div><div class="sub">Persistent World</div></div></div></div>
<nav class="harness-nav"><a class="active" href="#">Home</a><a href="#">News</a><a href="#">Gallery</a><a href="#">Dev Blog</a><a href="#">Forums</a><a href="#">Wiki</a></nav>
<div class="harness-content" id="slot">Loading widget…</div>
<script>
fetch('home.html').then(r=>r.text()).then(html=>{
// strip the leading HTML comment block, keep <style> + markup
document.getElementById('slot').innerHTML = html.replace(/^<!--[\s\S]*?-->/,'');
});
</script>
</body>
</html>
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Westgate page preview (theme harness)</title>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=Jost:wght@300;400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
/* --- Simulated theme globals (mirror of scss/westgate/_tokens.scss :root) --- */
:root{
--wg-font-display:"Cinzel",Georgia,serif;
--wg-font-text:"Jost",system-ui,sans-serif;
--wg-font-ui:"Jost",system-ui,sans-serif;
--wg-font-code:"IBM Plex Mono",ui-monospace,monospace;
--wg-bg:#0f0d12;--wg-bg-deep:#09080b;--wg-panel:#18141d;--wg-panel-2:#110f15;
--wg-plum:#2a1222;--wg-plum-soft:#3a1830;--wg-gold:#c2a35a;--wg-gold-soft:#a8893f;
--wg-red:#8e3438;--wg-status-online:#7fb86a;
--wg-text:#e6e0d6;--wg-text-soft:#b9b2a6;--wg-text-muted:#9a9086;
--wg-border:rgba(194,163,90,.14);--wg-border-soft:rgba(194,163,90,.08);
--wg-velvet-panel:linear-gradient(100deg,rgba(42,18,34,.46) 0%,rgba(25,22,31,.95) 23%,rgba(18,16,23,.99) 62%,rgba(13,12,17,.98) 100%);
--wg-velvet-panel-hover:linear-gradient(100deg,rgba(58,24,48,.56) 0%,rgba(31,25,39,.98) 23%,rgba(21,18,27,1) 62%,rgba(15,14,20,.99) 100%);
--wg-velvet-shadow:inset 0 1px 0 rgba(255,255,255,.025),inset 0 0 34px rgba(96,32,68,.11),0 10px 26px rgba(0,0,0,.24);
}
html,body{margin:0;background:var(--wg-bg);color:var(--wg-text-soft);font-family:var(--wg-font-text);}
/* simulate Harmony brand hero banner + content container */
.harness-brand{max-width:1320px;margin:0 auto;padding:14px 20px 0;}
.harness-brand .bar{border:1px solid rgba(194,163,90,.22);border-radius:10px;background:linear-gradient(90deg,rgba(5,4,7,.82),rgba(10,8,12,0) 52%),#16121b;padding:18px 26px;display:flex;align-items:center;gap:14px;box-shadow:0 18px 42px rgba(0,0,0,.3);}
.harness-brand .logo{width:22px;height:22px;transform:rotate(45deg);border:1.5px solid var(--wg-gold);display:flex;align-items:center;justify-content:center;}
.harness-brand .logo i{width:7px;height:7px;background:var(--wg-gold);}
.harness-brand .ttl{font-family:var(--wg-font-display);font-weight:600;font-size:18px;letter-spacing:.12em;color:var(--wg-text);text-transform:uppercase;}
.harness-brand .sub{font-family:var(--wg-font-display);font-size:11px;letter-spacing:.22em;color:var(--wg-gold);text-transform:uppercase;}
.harness-nav{max-width:1320px;margin:10px auto 0;padding:0 20px;display:flex;gap:26px;}
.harness-nav a{font-family:var(--wg-font-ui);font-size:12.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--wg-text-soft);text-decoration:none;padding:8px 0;border-bottom:2px solid transparent;}
.harness-nav a.active{color:var(--wg-text);border-bottom-color:var(--wg-gold);}
.harness-content{max-width:1320px;margin:18px auto 0;padding:0 20px 60px;}
.harness-label{max-width:1320px;margin:0 auto;padding:10px 20px 0;font-family:var(--wg-font-code);font-size:11px;color:#6b6152;letter-spacing:.1em;text-transform:uppercase;}
</style>
</head>
<body>
<div class="harness-label">▾ Simulated theme chrome (brand hero + nav + #content) — the widget HTML is everything below the nav</div>
<div class="harness-brand"><div class="bar"><div class="logo"><i></i></div><div><div class="ttl">Shadows Over Westgate</div><div class="sub">Persistent World</div></div></div></div>
<nav class="harness-nav"><a href="#">News</a><a href="#">Gallery</a><a href="#">Wiki</a><a href="#">Forums</a><a href="#">Dev Blog</a></nav>
<div class="harness-content" id="slot">Loading widget…</div>
<script>
fetch('join-the-team.html').then(r=>r.text()).then(html=>{
// strip the leading HTML comment block, keep <style> + markup
document.getElementById('slot').innerHTML = html.replace(/^<!--[\s\S]*?-->/,'');
});
</script>
</body>
</html>
@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Westgate top bar preview (theme harness)</title>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=Jost:wght@300;400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
/* --- Simulated theme globals (mirror of scss/westgate/_tokens.scss :root) --- */
:root{
--wg-font-display:"Cinzel",Georgia,serif;
--wg-font-text:"Jost",system-ui,sans-serif;
--wg-font-ui:"Jost",system-ui,sans-serif;
--wg-font-code:"IBM Plex Mono",ui-monospace,monospace;
--wg-bg:#0f0d12;--wg-bg-deep:#09080b;--wg-panel:#18141d;--wg-panel-2:#110f15;
--wg-plum:#2a1222;--wg-plum-soft:#3a1830;--wg-gold:#c2a35a;--wg-gold-soft:#a8893f;
--wg-red:#8e3438;--wg-status-online:#7fb86a;
--wg-text:#e6e0d6;--wg-text-soft:#b9b2a6;--wg-text-muted:#9a9086;
--wg-border:rgba(194,163,90,.14);--wg-border-soft:rgba(194,163,90,.08);
--wg-velvet-panel:linear-gradient(100deg,rgba(42,18,34,.46) 0%,rgba(25,22,31,.95) 23%,rgba(18,16,23,.99) 62%,rgba(13,12,17,.98) 100%);
--wg-velvet-shadow:inset 0 1px 0 rgba(255,255,255,.025),inset 0 0 34px rgba(96,32,68,.11),0 10px 26px rgba(0,0,0,.24);
}
html,body{margin:0;background:var(--wg-bg);color:var(--wg-text-soft);font-family:var(--wg-font-text);}
/* dummy page body to demo sticky behaviour + full-width #panel */
.harness-page{max-width:1180px;margin:34px auto;padding:0 24px;}
.harness-page h1{font-family:var(--wg-font-display);font-weight:700;font-size:40px;color:#f3ede4;letter-spacing:.03em;margin:0 0 8px;}
.harness-page .eb{font-family:var(--wg-font-display);font-size:12px;letter-spacing:.28em;text-transform:uppercase;color:var(--wg-gold);}
.harness-page p{font-size:16px;line-height:1.9;color:var(--wg-text-muted);max-width:680px;}
.harness-block{height:240px;margin:20px 0;border:1px dashed rgba(194,163,90,.2);border-radius:8px;
display:flex;align-items:center;justify-content:center;font-family:var(--wg-font-code);font-size:12px;
letter-spacing:.1em;text-transform:uppercase;color:#6b6152;
background:repeating-linear-gradient(135deg,#1b1320,#1b1320 11px,#161019 11px,#161019 22px);}
</style>
</head>
<body>
<div id="slot">Loading top bar…</div>
<main class="harness-page">
<div class="eb">Simulated #panel content</div>
<h1>The Gates Open</h1>
<p>Scroll to confirm the bar stays pinned. This dummy body stands in for the full-width forum content that runs under the global top bar once the left sidebar is removed.</p>
<div class="harness-block">Forum content / widget area</div>
<div class="harness-block">Forum content / widget area</div>
</main>
<script>
fetch('top-bar.html').then(r=>r.text()).then(html=>{
var slot = document.getElementById('slot');
// strip leading HTML comment block, keep <style> + markup + <script>
slot.innerHTML = html.replace(/^[\s\S]*?-->/,'');
// re-execute injected <script> tags (innerHTML doesn't run them)
slot.querySelectorAll('script').forEach(function(old){
var s=document.createElement('script');
if(old.src) s.src=old.src; else s.textContent=old.textContent;
old.parentNode.replaceChild(s,old);
});
});
</script>
</body>
</html>
+238
View File
@@ -0,0 +1,238 @@
<!--
============================================================================
SHADOWS OVER WESTGATE — HOME (Custom Page content)
----------------------------------------------------------------------------
HOW TO USE (NodeBB Custom Pages + Widgets, theme: nodebb-theme-westgate)
1. ACP → Custom Pages: add a page with route "home" (or "/").
2. ACP → Extend → Widgets: pick the "Home" template, choose the main
content area, drag in an "HTML" widget, and paste THIS ENTIRE FILE in.
3. To run without the left sidebar on this page, leave the sidebar widget
area empty (Harmony hides an empty sidebar) and keep your nav links in
ACP → Navigation (Home / News / Gallery / Dev Blog / Forums / Wiki).
NOTES
- All colours/fonts use the theme's global --wg-* CSS variables, so this
block matches the forum + wiki and tracks any future palette change.
- Styles are scoped under .wg-page to avoid leaking into forum chrome.
- Image placeholders show a striped fill. To go live, copy each
data-src onto src (ACP → Uploads gives you the path). data-wg-art marks them.
- Server-status numbers are static here; wire them to your status API/widget
when ready (search "STATUS" below).
============================================================================
-->
<style>
.wg-page{
--page-bright:#f3ede4;
/* Full-width: the forum panel already provides left/right gutters, so content
fills the panel (matching the footer rule). Override per-block where a
narrower reading measure is wanted (e.g. .wg-pitch). */
--page-maxw:none;
font-family:var(--wg-font-text, "Jost", sans-serif);
color:var(--wg-text-soft, #b9b2a6);
}
.wg-page *{box-sizing:border-box;}
.wg-page .wg-wrap{max-width:var(--page-maxw);margin:0 auto;}
.wg-page a{text-decoration:none;}
.wg-page img[data-wg-art]{background:repeating-linear-gradient(135deg,#1b1320,#1b1320 11px,#161019 11px,#161019 22px);}
/* ---- Buttons ---- */
.wg-page .wg-btn{
display:inline-block;font-family:var(--wg-font-ui,"Jost",sans-serif);
font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-weight:600;
color:#150f08;background:linear-gradient(180deg,#d8bd76,#a8893f);
padding:14px 30px;border-radius:6px;border:1px solid rgba(216,194,138,.6);
box-shadow:0 4px 22px rgba(168,133,74,.28);transition:filter .2s,transform .2s;
}
.wg-page .wg-btn:hover{filter:brightness(1.07);transform:translateY(-1px);}
.wg-page .wg-btn-ghost{
display:inline-block;font-family:var(--wg-font-ui,"Jost",sans-serif);
font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-weight:600;
color:var(--wg-text,#e6e0d6);background:rgba(194,163,90,.06);
padding:14px 30px;border-radius:6px;border:1px solid rgba(194,163,90,.4);
transition:background .2s,border-color .2s;
}
.wg-page .wg-btn-ghost:hover{background:rgba(194,163,90,.12);border-color:rgba(194,163,90,.6);}
/* ---- Shared bits ---- */
.wg-page .wg-eyebrow{font-family:var(--wg-font-display,"Cinzel",serif);font-size:12px;
letter-spacing:.28em;text-transform:uppercase;color:var(--wg-gold,#c2a35a);}
.wg-page .wg-h2{font-family:var(--wg-font-display,"Cinzel",serif);font-weight:600;
font-size:34px;color:var(--page-bright);margin:0;letter-spacing:.02em;
text-shadow:0 1px 0 rgba(0,0,0,.9),0 0 10px rgba(104,32,76,.16);}
.wg-page .wg-card{background:var(--wg-velvet-panel);border:1px solid var(--wg-border,rgba(194,163,90,.14));
border-radius:8px;box-shadow:var(--wg-velvet-shadow);}
.wg-page .wg-rule{display:flex;align-items:center;justify-content:center;gap:18px;}
.wg-page .wg-rule i{height:1px;width:80px;display:block;}
.wg-page .wg-rule span{width:7px;height:7px;background:var(--wg-gold,#c2a35a);transform:rotate(45deg);}
/* ---- Hero ---- */
.wg-page .wg-hero{position:relative;overflow:hidden;border:1px solid rgba(194,163,90,.22);
border-radius:10px;min-height:420px;display:flex;flex-direction:column;align-items:center;
justify-content:center;text-align:center;padding:64px 24px;margin-top:.25rem;
box-shadow:inset 0 1px 0 rgba(255,242,204,.06),inset 0 0 0 1px rgba(33,21,28,.5),0 18px 42px rgba(0,0,0,.3);}
.wg-page .wg-hero__bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:0;}
.wg-page .wg-hero__veil{position:absolute;inset:0;z-index:1;pointer-events:none;
background:linear-gradient(180deg,rgba(9,8,11,.55) 0%,rgba(9,8,11,.25) 38%,rgba(9,8,11,.82) 86%,var(--wg-bg,#0f0d12) 100%);}
.wg-page .wg-hero__in{position:relative;z-index:2;}
.wg-page .wg-hero h1{font-family:var(--wg-font-display,"Cinzel",serif);font-weight:700;
font-size:clamp(40px,6vw,68px);line-height:1.04;letter-spacing:.03em;color:var(--page-bright);
margin:18px 0 0;text-shadow:0 4px 40px rgba(0,0,0,.7);}
.wg-page .wg-hero p{font-family:var(--wg-font-text,"Jost",sans-serif);font-weight:300;
font-size:19px;color:#cfc5b7;max-width:620px;margin:22px auto 0;line-height:1.6;
text-shadow:0 2px 16px rgba(0,0,0,.8);}
.wg-page .wg-hero__cta{display:flex;gap:16px;justify-content:center;flex-wrap:wrap;margin-top:34px;}
/* ---- Status bar ---- */
.wg-page .wg-status{margin-top:-40px;position:relative;z-index:5;padding:24px 32px;
display:flex;align-items:center;gap:36px;flex-wrap:wrap;}
.wg-page .wg-status__dot{width:12px;height:12px;border-radius:50%;background:var(--wg-status-online,#7fb86a);
box-shadow:0 0 0 4px rgba(127,184,106,.16);}
.wg-page .wg-status__sep{width:1px;height:40px;background:rgba(194,163,90,.18);}
.wg-page .wg-status .lbl{font-family:var(--wg-font-display,"Cinzel",serif);font-size:18px;
letter-spacing:.05em;color:var(--wg-text,#e6e0d6);}
.wg-page .wg-status .sub{font-family:var(--wg-font-code,"IBM Plex Mono",monospace);font-size:11px;
letter-spacing:.06em;color:var(--wg-text-muted,#9a9086);margin-top:3px;}
.wg-page .wg-status .big{font-family:var(--wg-font-display,"Cinzel",serif);font-size:25px;color:var(--wg-text,#e6e0d6);}
.wg-page .wg-status .big small{font-size:14px;color:var(--wg-text-muted,#9a9086);}
.wg-page .wg-status .mono{font-family:var(--wg-font-code,"IBM Plex Mono",monospace);font-size:14px;color:var(--wg-text-soft,#b9b2a6);margin-top:4px;}
/* ---- Pitch ---- */
.wg-page .wg-pitch{max-width:760px;margin:0 auto;padding:92px 24px 24px;text-align:center;}
.wg-page .wg-pitch .lead{font-family:var(--wg-font-text,"Jost",sans-serif);font-weight:300;
font-size:23px;line-height:1.7;color:#cfc5b7;margin:0 0 32px;}
.wg-page .wg-pitch p{font-family:var(--wg-font-text,"Jost",sans-serif);font-size:17px;
line-height:1.95;color:var(--wg-text-muted,#9a9086);margin:0 0 24px;}
.wg-page .wg-pitch blockquote{font-family:var(--wg-font-display,"Cinzel",serif);font-style:italic;
font-size:22px;line-height:1.5;color:#d8c28a;margin:44px auto;max-width:560px;}
.wg-page .wg-pitch .sign{font-family:var(--wg-font-display,"Cinzel",serif);font-size:19px;
letter-spacing:.04em;color:var(--wg-text,#e6e0d6);margin:32px 0 0;}
/* ---- Get started ---- */
.wg-page .wg-steps{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;}
.wg-page .wg-step{display:block;padding:28px 24px;transition:border-color .25s,transform .25s;}
.wg-page .wg-step:hover{border-color:rgba(194,163,90,.4)!important;transform:translateY(-4px);}
.wg-page .wg-step .ico{width:42px;height:42px;border:1px solid rgba(194,163,90,.4);border-radius:50%;
display:flex;align-items:center;justify-content:center;margin-bottom:20px;}
.wg-page .wg-step .ico i{width:12px;height:12px;border:1.5px solid var(--wg-gold,#c2a35a);transform:rotate(45deg);display:block;}
.wg-page .wg-step h3{font-family:var(--wg-font-display,"Cinzel",serif);font-size:19px;color:var(--wg-text,#e6e0d6);margin:0 0 10px;}
.wg-page .wg-step p{font-family:var(--wg-font-text,"Jost",sans-serif);font-size:14.5px;line-height:1.7;color:var(--wg-text-muted,#9a9086);margin:0;}
/* ---- News strip ---- */
.wg-page .wg-news{display:grid;grid-template-columns:repeat(3,1fr);gap:22px;}
.wg-page .wg-news article{overflow:hidden;transition:border-color .25s;}
.wg-page .wg-news article:hover{border-color:rgba(194,163,90,.4)!important;}
.wg-page .wg-news .thumb{height:160px;width:100%;object-fit:cover;display:block;border-bottom:1px solid rgba(194,163,90,.12);background:#161019;}
.wg-page .wg-news .body{padding:22px 24px;}
.wg-page .wg-news .meta{font-family:var(--wg-font-code,"IBM Plex Mono",monospace);font-size:11px;
letter-spacing:.08em;text-transform:uppercase;color:var(--wg-text-muted,#9a9086);margin-bottom:10px;}
.wg-page .wg-news h3{font-family:var(--wg-font-display,"Cinzel",serif);font-size:19px;line-height:1.35;color:var(--wg-text,#e6e0d6);margin:0 0 10px;}
.wg-page .wg-news p{font-family:var(--wg-font-text,"Jost",sans-serif);font-size:14px;line-height:1.7;color:var(--wg-text-muted,#9a9086);margin:0;}
.wg-page .wg-section{padding:72px 24px 0;}
.wg-page .wg-section.last{padding-bottom:96px;}
.wg-page .wg-section__head{text-align:center;margin-bottom:44px;}
.wg-page .wg-section__head .wg-eyebrow{display:block;margin-bottom:12px;}
.wg-page .wg-flex-head{display:flex;align-items:flex-end;justify-content:space-between;margin-bottom:32px;gap:16px;flex-wrap:wrap;}
.wg-page .wg-more{font-family:var(--wg-font-ui,"Jost",sans-serif);font-size:12px;letter-spacing:.12em;
text-transform:uppercase;color:var(--wg-gold,#c2a35a);border-bottom:1px solid rgba(194,163,90,.4);padding-bottom:3px;}
@media (max-width:900px){
.wg-page .wg-steps{grid-template-columns:repeat(2,1fr);}
.wg-page .wg-news{grid-template-columns:1fr;}
.wg-page .wg-status{gap:22px;}
.wg-page .wg-status__sep{display:none;}
}
</style>
<div class="wg-page wg-home">
<!-- ============ HERO ============ -->
<div class="wg-wrap">
<section class="wg-hero">
<!-- ART: replace src with your banner key art (wide, ~1600×640). -->
<img class="wg-hero__bg" data-wg-art src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" data-src="/assets/uploads/westgate/home-hero.jpg" alt="" onerror="this.style.display='none'">
<div class="wg-hero__veil"></div>
<div class="wg-hero__in">
<span class="wg-eyebrow">Neverwinter Nights · Persistent World</span>
<h1>Welcome to Westgate</h1>
<p>An ancient port city rises out of the mists, at the edge of the Sea of Fallen Stars. Mind the shadows.</p>
<div class="wg-hero__cta">
<a class="wg-btn" href="/wiki">How to Join</a>
<a class="wg-btn-ghost" href="/news">Latest News</a>
</div>
</div>
</section>
</div>
<!-- ============ SERVER STATUS ============ -->
<div class="wg-wrap">
<div class="wg-status wg-card">
<div style="display:flex;align-items:center;gap:14px;">
<span class="wg-status__dot"></span>
<div><div class="lbl">Server Online</div><div class="sub">ALPHA · MODULE 0.3</div></div>
</div>
<div class="wg-status__sep"></div>
<!-- STATUS: wire these numbers to your status feed when ready -->
<div><div class="big">14 <small>souls online</small></div><div class="sub">PEAKED 32 TODAY</div></div>
<div class="wg-status__sep"></div>
<div style="flex:1;min-width:160px;">
<div style="font-family:var(--wg-font-code,monospace);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--wg-gold,#c2a35a);">Direct Connect</div>
<div class="mono">play.westgate.pw</div>
</div>
<a class="wg-btn" href="/wiki">Connect Guide</a>
</div>
</div>
<!-- ============ PITCH ============ -->
<div class="wg-pitch">
<div class="wg-rule" style="margin-bottom:36px;"><i style="background:linear-gradient(90deg,transparent,var(--wg-gold,#c2a35a));"></i><span></span><i style="background:linear-gradient(90deg,var(--wg-gold,#c2a35a),transparent);"></i></div>
<p class="lead">Long ago, merchants from distant lands filled its harbors, and gold flowed as freely as wine — but time, as it does, has a way of curdling mortal splendor.</p>
<p>Today its bones are old stone and salt-worn docks, and bargains are struck beneath every lantern's glow. Here, hoarded secrets, owed favour, and unspoken truths are the true measure of power — although coin certainly helps.</p>
<p>Westgate is a city ever-balanced on a blade's edge, built upon decaying decadence. The masked and nameless guilds clot the veins of the city with their greed and cruelty. Noble houses cling to fading legacies as their bloodlines tangle with curses, ambition, and desperation. Old cults and buried powers hold the Undergate by its throat. Beneath it all, antediluvian things begin to stir — patient, watchful, and hungry.</p>
<blockquote>“To walk Westgate's streets is to wager your soul in a game of chance.”</blockquote>
<p>Every alley offers opportunity, yet each carries a price. While some come seeking fortune or power, others arrive hoping to disappear — and indeed they do, for in these boulevards you are lucky to merely survive. In Westgate, few leave unchanged… and fewer still leave at all.</p>
<p class="sign">Welcome to Westgate. Mind the shadows.</p>
</div>
<!-- ============ GET STARTED ============ -->
<div class="wg-wrap wg-section">
<div class="wg-section__head">
<span class="wg-eyebrow">Your First Steps</span>
<h2 class="wg-h2">Enter the City</h2>
</div>
<div class="wg-steps">
<a class="wg-step wg-card" href="/wiki"><div class="ico"><i></i></div><h3>How to Join</h3><p>Install Neverwinter Nights: EE, point it at our server, and step through the gate.</p></a>
<a class="wg-step wg-card" href="/wiki"><div class="ico"><i style="border-radius:0;"></i></div><h3>New Player Wiki</h3><p>Starter guides, character creation, and everything you need for your first night.</p></a>
<a class="wg-step wg-card" href="/wiki"><div class="ico"><i style="border-radius:50%;transform:none;"></i></div><h3>The Setting Bible</h3><p>Lore of Westgate — its districts, guilds, noble houses, and buried histories.</p></a>
<a class="wg-step wg-card" href="/categories"><div class="ico"><i style="transform:none;border-radius:2px;"></i></div><h3>Join the Community</h3><p>Introduce yourself in the forums and find your place among Westgate's denizens.</p></a>
</div>
</div>
<!-- ============ LATEST NEWS ============ -->
<div class="wg-wrap wg-section last">
<div class="wg-flex-head">
<div>
<span class="wg-eyebrow" style="display:block;margin-bottom:10px;">From the Watch</span>
<h2 class="wg-h2">Latest News</h2>
</div>
<a class="wg-more" href="/news">All Dispatches →</a>
</div>
<div class="wg-news">
<article class="wg-card">
<a href="/category/1/news"><img class="thumb" data-wg-art src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" data-src="/assets/uploads/westgate/news-1.jpg" alt="" onerror="this.style.visibility='hidden'"></a>
<div class="body"><div class="meta">24 Jun 2026 · Announcement</div><h3>The Gates Open — Alpha Has Begun</h3><p>Westgate opens its harbour to its first wave of adventurers. Everything you need to know.</p></div>
</article>
<article class="wg-card">
<a href="/category/1/news"><img class="thumb" data-wg-art src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" data-src="/assets/uploads/westgate/news-2.jpg" alt="" onerror="this.style.visibility='hidden'"></a>
<div class="body"><div class="meta">21 Jun 2026 · Patch Note</div><h3>Patch 0.3 — The Undergate</h3><p>The tunnels beneath the city are now open. New areas, factions, and dangers await below.</p></div>
</article>
<article class="wg-card">
<a href="/category/1/news"><img class="thumb" data-wg-art src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" data-src="/assets/uploads/westgate/news-3.jpg" alt="" onerror="this.style.visibility='hidden'"></a>
<div class="body"><div class="meta">18 Jun 2026 · Community</div><h3>The Community Charter</h3><p>Our revised rules and the principles that keep Westgate's streets worth walking.</p></div>
</article>
</div>
</div>
</div>
@@ -0,0 +1,172 @@
<!--
============================================================================
SHADOWS OVER WESTGATE — JOIN THE TEAM / Recruitment (Custom Page content)
----------------------------------------------------------------------------
HOW TO USE (NodeBB Custom Pages + Widgets, theme: nodebb-theme-westgate)
1. ACP -> Custom Pages: add a page with route "join" (or "recruitment").
2. ACP -> Extend -> Widgets: pick that page's template, choose the main
content area, drag in an "HTML" widget, and paste THIS ENTIRE FILE in.
3. Sidebar-off + nav links: same as the Home page (see home.html notes).
NOTES
- Shares the SAME scoped vocabulary as home.html (.wg-page, .wg-btn,
.wg-card, .wg-eyebrow, .wg-rule, .wg-h2). If both pages ever live in one
bundle the styles are identical and de-dupe cleanly.
- All colours/fonts use the theme's global --wg-* variables.
- DISCORD: replace every href="#discord" with your real invite link.
- Image placeholders show a striped fill. To go live, copy each data-src
onto src (data-wg-art marks them).
============================================================================
-->
<style>
.wg-page{
--page-bright:#f3ede4;
/* Full-width: the forum panel already provides left/right gutters, so content
fills the panel (matching the footer rule). Override per-block where a
narrower reading measure is wanted (e.g. .wg-cta). */
--page-maxw:none;
font-family:var(--wg-font-text,"Jost",sans-serif);
color:var(--wg-text-soft,#b9b2a6);
}
.wg-page *{box-sizing:border-box;}
.wg-page .wg-wrap{max-width:var(--page-maxw);margin:0 auto;}
.wg-page a{text-decoration:none;}
.wg-page img[data-wg-art]{background:repeating-linear-gradient(135deg,#1b1320,#1b1320 11px,#161019 11px,#161019 22px);}
.wg-page .wg-btn{display:inline-block;font-family:var(--wg-font-ui,"Jost",sans-serif);
font-size:13px;letter-spacing:.1em;text-transform:uppercase;font-weight:600;color:#150f08;
background:linear-gradient(180deg,#d8bd76,#a8893f);padding:14px 30px;border-radius:6px;
border:1px solid rgba(216,194,138,.6);box-shadow:0 4px 22px rgba(168,133,74,.28);
transition:filter .2s,transform .2s;}
.wg-page .wg-btn:hover{filter:brightness(1.07);transform:translateY(-1px);}
.wg-page .wg-eyebrow{font-family:var(--wg-font-display,"Cinzel",serif);font-size:12px;
letter-spacing:.28em;text-transform:uppercase;color:var(--wg-gold,#c2a35a);}
.wg-page .wg-h2{font-family:var(--wg-font-display,"Cinzel",serif);font-weight:600;
font-size:34px;color:var(--page-bright);margin:0;letter-spacing:.02em;
text-shadow:0 1px 0 rgba(0,0,0,.9),0 0 10px rgba(104,32,76,.16);}
.wg-page .wg-card{background:var(--wg-velvet-panel);border:1px solid var(--wg-border,rgba(194,163,90,.14));
border-radius:8px;box-shadow:var(--wg-velvet-shadow);}
.wg-page .wg-rule{display:flex;align-items:center;justify-content:center;gap:18px;}
.wg-page .wg-rule i{height:1px;width:80px;display:block;}
.wg-page .wg-rule span{width:7px;height:7px;background:var(--wg-gold,#c2a35a);transform:rotate(45deg);}
.wg-page .wg-section{padding:80px 24px 0;}
.wg-page .wg-section.last{padding-bottom:100px;}
.wg-page .wg-section__head{text-align:center;margin-bottom:44px;}
.wg-page .wg-section__head .wg-eyebrow{display:block;margin-bottom:12px;}
/* hero */
.wg-page .wg-rhero{display:grid;grid-template-columns:1.05fr 1fr;gap:56px;align-items:center;padding:82px 24px 20px;}
.wg-page .wg-rhero h1{font-family:var(--wg-font-display,"Cinzel",serif);font-weight:700;
font-size:clamp(36px,4.6vw,52px);line-height:1.08;letter-spacing:.02em;color:var(--page-bright);margin:0 0 22px;}
.wg-page .wg-rhero .lead{font-family:var(--wg-font-text,"Jost",sans-serif);font-weight:300;
font-size:18px;line-height:1.7;color:#cfc5b7;margin:0 0 34px;max-width:30em;}
.wg-page .wg-shot{overflow:hidden;}
.wg-page .wg-shot .bar{display:flex;align-items:center;gap:9px;padding:13px 16px;border-bottom:1px solid rgba(194,163,90,.16);}
.wg-page .wg-shot .bar i{width:9px;height:9px;border-radius:50%;display:block;}
.wg-page .wg-shot .bar span{font-family:var(--wg-font-code,"IBM Plex Mono",monospace);font-size:12px;color:var(--wg-text-muted,#9a9086);margin-left:6px;}
.wg-page .wg-shot img{width:100%;aspect-ratio:16/11;object-fit:cover;display:block;background:#161019;}
/* pillars */
.wg-page .wg-pillars{display:grid;grid-template-columns:repeat(3,1fr);gap:22px;}
.wg-page .wg-pillar{overflow:hidden;display:flex;flex-direction:column;}
.wg-page .wg-pillar img{width:100%;aspect-ratio:16/9;object-fit:cover;display:block;border-bottom:1px solid rgba(194,163,90,.12);background:#161019;}
.wg-page .wg-pillar .body{padding:26px 26px 28px;}
.wg-page .wg-pillar h3{font-family:var(--wg-font-display,"Cinzel",serif);font-size:20px;color:var(--wg-text,#e6e0d6);margin:0 0 10px;}
.wg-page .wg-pillar p{font-family:var(--wg-font-text,"Jost",sans-serif);font-size:14.5px;line-height:1.7;color:var(--wg-text-muted,#9a9086);margin:0;}
/* team panel */
.wg-page .wg-team{padding:46px 50px;display:grid;grid-template-columns:1.1fr 1fr;gap:50px;align-items:center;}
.wg-page .wg-team h2{font-family:var(--wg-font-display,"Cinzel",serif);font-weight:600;font-size:30px;line-height:1.2;color:var(--page-bright);margin:0 0 16px;}
.wg-page .wg-team p{font-family:var(--wg-font-text,"Jost",sans-serif);font-size:16px;line-height:1.85;color:var(--wg-text-muted,#9a9086);margin:0;}
.wg-page .wg-team ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:16px;}
.wg-page .wg-team li{display:flex;align-items:center;gap:14px;font-family:var(--wg-font-text,"Jost",sans-serif);font-size:15.5px;color:var(--wg-text-soft,#b9b2a6);}
.wg-page .wg-team li b{width:7px;height:7px;background:var(--wg-gold,#c2a35a);transform:rotate(45deg);flex:none;}
/* cta */
.wg-page .wg-cta{max-width:760px;margin:0 auto;padding:80px 24px 100px;text-align:center;}
.wg-page .wg-cta h2{font-family:var(--wg-font-display,"Cinzel",serif);font-weight:700;font-size:40px;color:var(--page-bright);margin:0 0 16px;letter-spacing:.02em;}
.wg-page .wg-cta .lead{font-family:var(--wg-font-text,"Jost",sans-serif);font-weight:300;font-size:18px;line-height:1.7;color:#cfc5b7;margin:0 0 30px;}
.wg-page .wg-cta .foot{margin-top:46px;padding-top:22px;border-top:1px solid rgba(194,163,90,.12);font-family:var(--wg-font-code,"IBM Plex Mono",monospace);font-size:12px;letter-spacing:.06em;color:var(--wg-text-muted,#9a9086);}
@media (max-width:900px){
.wg-page .wg-rhero{grid-template-columns:1fr;gap:32px;}
.wg-page .wg-pillars{grid-template-columns:1fr;}
.wg-page .wg-team{grid-template-columns:1fr;gap:28px;padding:32px;}
}
</style>
<div class="wg-page wg-recruit">
<!-- ============ HERO ============ -->
<div class="wg-wrap">
<div class="wg-rhero">
<div>
<span class="wg-eyebrow">Persistent World · Made by Hand</span>
<h1 style="margin-top:22px;">Come build a world that's a pleasure to work on.</h1>
<p class="lead">Westgate is made by a handful of people who love this stuff — artists, writers, builders, scripters. We've kept the tools pleasant so your time goes where it matters: into the world itself. If you make things, we'd love to meet you.</p>
<!-- DISCORD: replace href -->
<a class="wg-btn" href="#discord">Say Hello on Discord</a>
</div>
<div class="wg-card wg-shot">
<div class="bar"><i style="background:#7fb86a;"></i><i style="background:#d8bd76;"></i><i style="background:#bf8a86;"></i><span>westgate — the undergate</span></div>
<!-- ART: a screenshot of a corner contributors helped build -->
<img data-wg-art src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" data-src="/assets/uploads/westgate/recruit-shot.jpg" alt="" onerror="this.style.visibility='hidden'">
</div>
</div>
</div>
<!-- ============ WHO WE NEED ============ -->
<div class="wg-wrap wg-section">
<div class="wg-section__head">
<span class="wg-eyebrow">Who We Need</span>
<h2 class="wg-h2">Every kind of maker has a place here</h2>
<p style="font-family:var(--wg-font-text,'Jost',sans-serif);font-size:16px;line-height:1.7;color:var(--wg-text-muted,#9a9086);margin:14px auto 0;max-width:32em;">Code, art, words, world — whatever you make, there's a part of Westgate waiting for it.</p>
</div>
<div class="wg-pillars">
<article class="wg-card wg-pillar">
<img data-wg-art src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" data-src="/assets/uploads/westgate/recruit-scripts.jpg" alt="" onerror="this.style.visibility='hidden'">
<div class="body"><h3>Tools that stay out of your way</h3><p>For builders and scripters: a clean toolset and modern scripting on a fast core — none of NWScript's old quirks to fight. Everything's saved and versioned, so you can experiment without fear of breaking things.</p></div>
</article>
<article class="wg-card wg-pillar">
<img data-wg-art src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" data-src="/assets/uploads/westgate/recruit-art.jpg" alt="" onerror="this.style.visibility='hidden'">
<div class="body"><h3>Your craft, your stamp</h3><p>For artists and asset-makers: models, textures, areas, sound. We're not after a house style to copy — we want the thing only you'd make, and we'll help get it into the game.</p></div>
</article>
<article class="wg-card wg-pillar">
<img data-wg-art src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" data-src="/assets/uploads/westgate/recruit-lore.jpg" alt="" onerror="this.style.visibility='hidden'">
<div class="body"><h3>Ideas that reach players</h3><p>For writers and designers: a questline, a faction, a forgotten corner of the city. What you dream up this month is something players are living in the next.</p></div>
</article>
</div>
</div>
<!-- ============ WHAT IT'S LIKE ============ -->
<div class="wg-wrap wg-section">
<div class="wg-card wg-team">
<div>
<span class="wg-eyebrow">What It's Actually Like</span>
<h2 style="margin-top:14px;">Low ego, high trust — and your work goes live.</h2>
<p>No gatekeeping, no hazing, no "figure it out yourself." We give honest, kind feedback, lend a hand when it helps, and keep things moving. The thing you make this week is the thing players see this week.</p>
</div>
<ul>
<li><b></b>Honest, kind feedback on your work</li>
<li><b></b>A hand whenever you want one</li>
<li><b></b>What you make can be in-game within the week</li>
<li><b></b>Work at whatever pace fits your life</li>
</ul>
</div>
</div>
<!-- ============ CTA ============ -->
<div class="wg-cta">
<div class="wg-rule" style="margin-bottom:36px;"><i style="background:linear-gradient(90deg,transparent,var(--wg-gold,#c2a35a));"></i><span></span><i style="background:linear-gradient(90deg,var(--wg-gold,#c2a35a),transparent);"></i></div>
<h2>Sound like your kind of place?</h2>
<p class="lead">No test gauntlet, no résumé. Just come say hi and tell us what you'd love to build.</p>
<!-- DISCORD: replace href -->
<a class="wg-btn" href="#discord" style="padding:16px 36px;box-shadow:0 4px 26px rgba(168,133,74,.4);">Join the Team on Discord →</a>
<div class="foot">Artists · writers · builders · scripters — all welcome</div>
</div>
</div>
+886
View File
@@ -0,0 +1,886 @@
<!--
============================================================================
SHADOWS OVER WESTGATE — GLOBAL TOP BAR (theme header partial)
----------------------------------------------------------------------------
Visual target: "Westgate Top Bar.dc.html" (member / guest / Forums menu /
user menu / mobile drawer). Theme: nodebb-theme-westgate.
WHAT THIS IS
- A self-contained, modular header: markup + scoped CSS + a small vanilla-JS
controller. Drop-in for previewing AND a faithful reference for the live
NodeBB partial described in THEME-INTEGRATION.md §4.
HOW TO USE IT LIVE (templates/partials/header/topbar.tpl)
1. Paste the <style> block once (or move it into scss/westgate/_topbar.scss).
2. Paste the <header class="wg-topbar"> markup into the partial. Replace the
two demo clusters with NodeBB conditionals:
{{{ if loggedIn }}} ...member right-cluster...
{{{ else }}} ...guest right-cluster... {{{ end }}}
3. NAV ITEMS: loop ACP -> Navigation instead of the hard-coded <a>s. The
"Forums" item keeps the data-wg-menu="forums" hook to open the mega-menu;
populate its panel from your real category list.
4. LIVE COMPONENTS: swap the static notification / chat / user dropdowns for
Harmony's includes (partials/header/menu.tpl etc.) so counts stay live.
The data-wg-menu / data-wg-panel hooks below show exactly where each goes.
5. Mount in header.tpl (replace the IMPORT of partials/sidebar-left.tpl) and
drop the left layout column so #panel runs full width.
6. JS: the inline <script> at the bottom is for this self-contained preview.
In production put the controller in ACP -> Custom JavaScript (see the
paste-ready "topbar-custom.js" in this folder) — NodeBB is an SPA, so it
uses document-level event delegation to survive page navigation, and the
preview-only member/guest switch is dropped there.
NOTES
- All colours/fonts use the theme's global --wg-* variables (with fallbacks),
so the bar tracks the forum + wiki palette.
- Everything is scoped under .wg-topbar so it can't leak into forum chrome.
- The little floating switch (bottom-right) is PREVIEW-ONLY — it flips
member/guest and is wrapped in .wg-topbar-demo. Delete that block + its
markup for production; nothing else depends on it.
- Avatar / badge counts are placeholders here.
============================================================================
-->
<!-- Preview-only: load brand fonts. The live theme already provides these, so
this <link> is harmless (de-dupes by URL) but can be removed in the partial. -->
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&amp;family=Jost:wght@300;400;500;600&amp;family=IBM+Plex+Mono:wght@400;500&amp;display=swap" rel="stylesheet">
<style>
/* ===================== GLOBAL TOP BAR ===================== */
.wg-topbar{
--tb-h:66px;
--tb-gold:var(--wg-gold,#c2a35a);
--tb-text:var(--wg-text,#e6e0d6);
--tb-soft:var(--wg-text-soft,#b9b2a6);
--tb-muted:var(--wg-text-muted,#9a9086);
--tb-bg:var(--wg-bg,#0f0d12);
--tb-border:rgba(194,163,90,.16);
--tb-presence:#7fb86a; /* status ring colour, switched by [data-presence] below */
position:sticky;top:0;z-index:1000;
font-family:var(--wg-font-text,"Jost",sans-serif);
background:var(--tb-bg);
border-bottom:1px solid var(--tb-border);
}
.wg-topbar[data-presence="online"] {--tb-presence:#7fb86a;}
.wg-topbar[data-presence="away"] {--tb-presence:#d8bd76;}
.wg-topbar[data-presence="dnd"] {--tb-presence:#bf6a66;}
.wg-topbar[data-presence="offline"] {--tb-presence:#6b6152;}
.wg-topbar *{box-sizing:border-box;}
.wg-topbar a{text-decoration:none;color:inherit;}
/* NB: use font-family (not the `font` shorthand) so this rule's specificity
doesn't reset font-size on text buttons like the Forums nav trigger — the
button's own class (e.g. .wg-topbar__link) sets the size. */
.wg-topbar button{font-family:inherit;line-height:inherit;color:inherit;background:none;border:0;cursor:pointer;padding:0;}
/* ---- bar shell ---- */
.wg-topbar__bar{
height:var(--tb-h);width:100%;margin:0 auto;
display:flex;align-items:center;justify-content:space-between;
padding:0 26px;gap:24px;
background:linear-gradient(180deg,rgba(20,17,25,.9),rgba(15,13,18,.92));
}
.wg-topbar__left{display:flex;align-items:center;gap:36px;min-width:0;}
/* ---- brand ---- */
.wg-topbar__brand{display:flex;align-items:center;gap:12px;flex:none;}
.wg-topbar__glyph{
width:24px;height:24px;transform:rotate(45deg);border:1.5px solid var(--tb-gold);
display:flex;align-items:center;justify-content:center;
box-shadow:0 0 12px rgba(194,163,90,.22);
}
.wg-topbar__glyph i{width:8px;height:8px;background:var(--tb-gold);display:block;}
.wg-topbar__name{
font-family:var(--wg-font-display,"Cinzel",serif);font-weight:600;font-size:15px;
letter-spacing:.14em;color:var(--tb-text);white-space:nowrap;
}
.wg-topbar__name--short{display:none;}
/* ---- primary nav ---- */
.wg-topbar__nav{display:flex;align-items:center;gap:26px;}
.wg-topbar__link{
position:relative;display:flex;align-items:center;gap:6px;
font-size:12.5px;letter-spacing:.13em;text-transform:uppercase;
color:var(--tb-soft);padding:4px 0;white-space:nowrap;
transition:color .2s;
}
.wg-topbar__link:hover,.wg-topbar__link.is-active{color:var(--tb-text);}
.wg-topbar__link .caret{transition:transform .25s;}
.wg-topbar__link[aria-expanded="true"] .caret{transform:rotate(180deg);}
.wg-topbar__link[aria-expanded="true"]{color:var(--tb-text);}
.wg-topbar__link::after{
content:"";position:absolute;left:-3px;right:-3px;bottom:-21px;height:2px;
background:linear-gradient(90deg,transparent,var(--tb-gold),transparent);
opacity:0;transform:scaleX(.4);transition:opacity .25s,transform .25s;
}
.wg-topbar__link.is-active::after,
.wg-topbar__link[aria-expanded="true"]::after{opacity:1;transform:scaleX(1);}
/* ---- right cluster ---- */
.wg-topbar__right{display:flex;align-items:center;gap:16px;flex:none;}
.wg-topbar__icon{
position:relative;width:34px;height:34px;border-radius:6px;
display:flex;align-items:center;justify-content:center;
color:var(--tb-soft);transition:background .2s,border-color .2s,color .2s;
}
.wg-topbar__icon:hover{color:var(--tb-text);background:rgba(194,163,90,.08);}
.wg-topbar__icon--search{border:1px solid rgba(194,163,90,.18);background:rgba(194,163,90,.05);}
.wg-topbar__badge{
position:absolute;top:-2px;right:-2px;min-width:15px;height:15px;border-radius:8px;
background:linear-gradient(180deg,#d8bd76,#a8893f);color:#150f08;
font-family:var(--wg-font-code,"IBM Plex Mono",monospace);font-size:9px;font-weight:600;
display:flex;align-items:center;justify-content:center;padding:0 3px;
border:1px solid var(--tb-bg);
}
/* ---- auth (guest) ---- */
.wg-topbar__login{font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--tb-soft);transition:color .2s;}
.wg-topbar__login:hover{color:var(--tb-text);}
.wg-topbar__register{
font-size:12px;letter-spacing:.1em;text-transform:uppercase;font-weight:600;color:#150f08;
background:linear-gradient(180deg,#d8bd76,#a8893f);padding:9px 20px;border-radius:6px;
border:1px solid rgba(216,194,138,.6);box-shadow:0 2px 12px rgba(168,133,74,.3);
transition:filter .2s,transform .2s;
}
.wg-topbar__register:hover{filter:brightness(1.07);transform:translateY(-1px);}
/* ---- avatar ---- */
.wg-topbar__avatar{
width:36px;height:36px;border-radius:50%;flex:none;overflow:hidden;
display:flex;align-items:center;justify-content:center;
background:radial-gradient(circle at 34% 24%,rgba(255,238,190,.22),transparent 30%),linear-gradient(145deg,#6f562d,#2c1122);
border:1px solid rgba(216,194,138,.4);
/* outer ring colour follows the chosen presence/status */
box-shadow:0 0 0 2px var(--tb-bg),0 0 0 3px var(--tb-presence);
background-size:cover;background-position:center;
}
.wg-topbar__avatar img{width:100%;height:100%;object-fit:cover;display:block;}
.wg-topbar__avatar-letter{font-family:var(--wg-font-display,"Cinzel",serif);font-size:15px;font-weight:600;color:#ecdcb6;line-height:1;}
/* the menu-header avatar shows the picture but no status ring */
.wg-usermenu__head .av{box-shadow:none;}
.wg-usermenu__head .av .wg-topbar__avatar-letter{font-size:17px;}
/* ---- pop-outs (drafts / notifications / chats) ---- */
.wg-topbar__popout{position:relative;display:flex;align-items:center;}
/* 360px + 400px-max scroll list = Harmony's .chats/.notifications/.drafts-dropdown */
.wg-topbar__panel--list{top:calc(100% + 14px);right:0;width:360px;padding:8px;}
.wg-pop__list{max-height:400px;overflow-y:auto;overscroll-behavior:contain;padding-right:2px;}
.wg-pop__list::-webkit-scrollbar{width:8px;}
.wg-pop__list::-webkit-scrollbar-thumb{background:rgba(194,163,90,.22);border-radius:4px;}
.wg-pop__sep{border:0;border-top:1px solid rgba(194,163,90,.1);margin:4px 0;}
.wg-ava{
width:32px;height:32px;border-radius:50%;flex:none;display:flex;align-items:center;justify-content:center;
font-family:var(--wg-font-display,"Cinzel",serif);font-size:13px;font-weight:600;color:#f1e6cf;
background:var(--c,#4a3a2a);box-shadow:inset 0 0 0 1px rgba(255,255,255,.08);
}
.wg-timeago{font-family:var(--wg-font-code,"IBM Plex Mono",monospace);font-size:11px;letter-spacing:.02em;color:var(--tb-muted);}
/* notification filter tabs */
.wg-pop__tabs{display:flex;gap:6px;padding:4px 4px 2px;}
.wg-pop__tab{
font-size:12px;letter-spacing:.04em;color:var(--tb-soft);padding:6px 14px;border-radius:6px;
display:flex;align-items:center;gap:7px;transition:background .18s,color .18s;
}
.wg-pop__tab:hover{background:rgba(194,163,90,.07);color:var(--tb-text);}
.wg-pop__tab.is-active{background:rgba(194,163,90,.12);color:var(--tb-text);font-weight:500;}
.wg-pop__tabcount{font-family:var(--wg-font-code,"IBM Plex Mono",monospace);font-size:10px;color:var(--tb-gold);}
/* notification row */
.wg-notif{display:flex;gap:11px;align-items:flex-start;padding:11px 10px;border-radius:6px;transition:background .18s;}
.wg-notif:hover{background:rgba(194,163,90,.07);}
.wg-notif.unread{background:rgba(194,163,90,.045);}
.wg-notif__body{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1;}
.wg-notif__text{font-family:var(--wg-font-text,"Jost",sans-serif);font-size:13px;line-height:1.45;color:var(--tb-soft);
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}
.wg-notif__text strong{color:var(--tb-text);font-weight:600;}
.wg-notif__dot{width:8px;height:8px;border-radius:50%;background:var(--tb-gold);flex:none;margin-top:5px;}
/* chat row */
.wg-chat{display:flex;gap:11px;align-items:flex-start;padding:11px 10px;border-radius:6px;transition:background .18s;}
.wg-chat:hover{background:rgba(194,163,90,.07);}
.wg-chat.unread{background:rgba(194,163,90,.045);}
.wg-chat__body{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1;}
.wg-chat__top{display:flex;align-items:baseline;justify-content:space-between;gap:8px;}
.wg-chat__name{font-family:var(--wg-font-display,"Cinzel",serif);font-size:13.5px;color:var(--tb-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wg-chat__last{font-family:var(--wg-font-text,"Jost",sans-serif);font-size:12.5px;color:var(--tb-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wg-chat__last strong{color:var(--tb-soft);font-weight:600;}
/* draft row */
.wg-draft{display:flex;gap:6px;align-items:flex-start;padding:9px 8px;border-radius:6px;transition:background .18s;}
.wg-draft:hover{background:rgba(194,163,90,.07);}
.wg-draft__open{display:flex;flex-direction:column;gap:6px;flex:1;min-width:0;}
.wg-draft__title{font-family:var(--wg-font-text,"Jost",sans-serif);font-size:13px;font-weight:600;color:var(--tb-text);
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}
.wg-draft__text{font-family:var(--wg-font-text,"Jost",sans-serif);font-size:12.5px;line-height:1.5;color:var(--tb-muted);
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}
.wg-draft__meta{display:flex;align-items:center;gap:9px;}
.wg-cat-label{font-family:var(--wg-font-code,"IBM Plex Mono",monospace);font-size:10px;letter-spacing:.06em;text-transform:uppercase;
color:var(--tb-gold);border:1px solid rgba(194,163,90,.3);border-radius:4px;padding:2px 7px;}
.wg-draft__del{width:26px;height:26px;border-radius:6px;flex:none;display:flex;align-items:center;justify-content:center;
color:var(--tb-muted);transition:background .18s,color .18s;}
.wg-draft__del:hover{background:rgba(191,138,134,.12);color:#d39c98;}
/* empty state */
.wg-pop__empty{display:flex;flex-direction:column;align-items:center;gap:12px;padding:34px 12px;color:var(--tb-muted);
font-family:var(--wg-font-text,"Jost",sans-serif);font-size:13px;}
.wg-pop__empty[hidden]{display:none;}
/* quick-search results (Harmony search.tpl quick-search-container) */
.wg-search__results{display:none;margin-top:8px;border-top:1px solid rgba(194,163,90,.12);padding-top:6px;}
.wg-topbar.is-searching .wg-topbar__searchbox.has-results{height:auto;flex-direction:column;align-items:stretch;padding:10px 12px;}
.wg-topbar__searchbox.has-results .wg-search__results{display:block;}
.wg-search__row{display:flex;gap:9px;align-items:flex-start;padding:8px 6px;border-radius:6px;transition:background .18s;}
.wg-search__row:hover{background:rgba(194,163,90,.08);}
.wg-search__row .t{font-family:var(--wg-font-text,"Jost",sans-serif);font-size:13px;color:var(--tb-text);line-height:1.4;}
.wg-search__row .m{font-family:var(--wg-font-code,"IBM Plex Mono",monospace);font-size:10.5px;letter-spacing:.04em;color:var(--tb-muted);margin-top:3px;}
.wg-search__all{display:block;text-align:center;font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--tb-gold);padding:9px;border-radius:6px;}
.wg-search__all:hover{background:rgba(194,163,90,.08);}
/* pop-out footer (mark-all-read / see-all) */
.wg-pop__foot{display:flex;gap:7px;padding:4px 2px 2px;}
.wg-pop__btn{flex:1;display:flex;align-items:center;justify-content:center;gap:7px;
font-size:12px;letter-spacing:.03em;color:var(--tb-soft);padding:8px 10px;border-radius:6px;
background:rgba(194,163,90,.06);transition:background .18s,color .18s,filter .18s;}
.wg-pop__btn:hover{background:rgba(194,163,90,.12);color:var(--tb-text);}
.wg-pop__btn--primary{background:linear-gradient(180deg,#d8bd76,#a8893f);color:#150f08;font-weight:600;}
.wg-pop__btn--primary:hover{filter:brightness(1.07);color:#150f08;}
.wg-topbar__searchbox{
display:flex;align-items:center;gap:10px;overflow:hidden;
width:0;opacity:0;padding:0;border:1px solid transparent;border-radius:6px;
background:rgba(8,7,10,.55);
transition:width .28s ease,opacity .2s,padding .28s,border-color .2s;
}
.wg-topbar.is-searching .wg-topbar__searchbox{width:240px;opacity:1;padding:0 12px;border-color:rgba(194,163,90,.18);height:34px;}
.wg-topbar.is-searching .wg-topbar__icon--search{display:none;}
.wg-topbar__searchbox input{
flex:1;min-width:0;background:none;border:0;outline:none;color:var(--tb-text);
font-family:inherit;font-size:13px;letter-spacing:.02em;
}
.wg-topbar__searchbox input::placeholder{color:var(--tb-muted);}
/* ===================== DROPDOWN PANELS ===================== */
.wg-topbar__panel{
position:absolute;z-index:1100;
background:linear-gradient(100deg,rgba(42,18,34,.5),rgba(25,22,31,.97) 30%,rgba(18,16,23,.99));
border:1px solid rgba(194,163,90,.2);border-radius:8px;
box-shadow:inset 0 1px 0 rgba(255,255,255,.03),0 18px 40px rgba(0,0,0,.5);
opacity:0;visibility:hidden;transform:translateY(-8px);
transition:opacity .2s,transform .2s,visibility 0s linear .2s;
}
.wg-topbar__panel.is-open{opacity:1;visibility:visible;transform:translateY(0);transition:opacity .2s,transform .2s,visibility 0s;}
/* forums mega-menu */
.wg-topbar__panel--forums{top:calc(var(--tb-h) - 4px);left:290px;width:560px;padding:10px;}
.wg-mega__grid{display:grid;grid-template-columns:1fr 1fr;gap:4px;}
.wg-mega__item{display:block;padding:13px 16px;border-radius:6px;transition:background .18s;}
.wg-mega__item:hover{background:rgba(194,163,90,.08);}
.wg-mega__item .t{font-family:var(--wg-font-display,"Cinzel",serif);font-size:15px;color:var(--tb-text);margin-bottom:3px;}
.wg-mega__item .d{font-size:12.5px;color:var(--tb-muted);}
.wg-mega__foot{
display:flex;align-items:center;justify-content:space-between;
margin-top:8px;padding:14px 16px 6px;border-top:1px solid rgba(194,163,90,.12);
}
.wg-mega__foot .links{display:flex;gap:20px;}
.wg-mega__foot .links a{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--tb-soft);transition:color .2s;}
.wg-mega__foot .links a:hover{color:var(--tb-text);}
.wg-mega__foot .all{font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--tb-gold);}
/* user menu */
.wg-topbar__usermenu{position:relative;}
.wg-topbar__panel--user{top:calc(var(--tb-h) - 2px);right:26px;width:248px;overflow:hidden;}
.wg-usermenu__head{display:flex;align-items:center;gap:12px;padding:16px 18px;border-bottom:1px solid rgba(194,163,90,.12);}
.wg-usermenu__head .av{width:40px;height:40px;flex:none;}
.wg-usermenu__head .nm{font-family:var(--wg-font-display,"Cinzel",serif);font-size:15px;color:var(--tb-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wg-usermenu__head .vp{font-size:12px;color:var(--tb-gold);}
.wg-usermenu__list{padding:8px;}
.wg-usermenu__row{
display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:6px;
font-size:14px;color:var(--tb-soft);transition:background .18s,color .18s;
}
.wg-usermenu__row:hover{background:rgba(194,163,90,.08);color:var(--tb-text);}
.wg-usermenu__row.danger{color:#bf8a86;}
.wg-usermenu__row.danger:hover{background:rgba(191,138,134,.1);color:#d39c98;}
.wg-usermenu__sep{height:1px;background:rgba(194,163,90,.12);margin:7px 4px;}
.wg-topbar__panel--forums .wg-mega__item.is-active,
.wg-usermenu__row.is-active{background:rgba(194,163,90,.06);}
.wg-usermenu__row{width:100%;text-align:left;}
.wg-usermenu__label{font-family:var(--wg-font-code,"IBM Plex Mono",monospace);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--tb-muted);padding:11px 12px 5px;}
.wg-usermenu__row .dot{width:11px;height:11px;border-radius:50%;flex:none;margin:0 2px;box-shadow:0 0 0 3px rgba(0,0,0,.25);}
.wg-usermenu__row .chk{margin-left:auto;color:var(--tb-gold);flex:none;}
.wg-usermenu__row svg{flex:none;}
/* ===================== MOBILE ===================== */
.wg-topbar__burger{display:none;flex-direction:column;gap:4px;width:24px;flex:none;}
.wg-topbar__burger span{width:20px;height:1.6px;background:var(--tb-gold);display:block;transition:transform .25s,opacity .25s;}
.wg-topbar.is-drawer .wg-topbar__burger span:nth-child(1){transform:translateY(5.6px) rotate(45deg);}
.wg-topbar.is-drawer .wg-topbar__burger span:nth-child(2){opacity:0;}
.wg-topbar.is-drawer .wg-topbar__burger span:nth-child(3){transform:translateY(-5.6px) rotate(-45deg);}
.wg-topbar__drawer{
display:none;overflow:hidden;max-height:0;
border-top:1px solid var(--tb-border);
transition:max-height .3s ease;
}
.wg-topbar.is-drawer .wg-topbar__drawer{max-height:560px;}
.wg-topbar__drawer-in{padding:10px 16px 18px;}
.wg-drawer__search{
display:flex;align-items:center;gap:10px;background:rgba(8,7,10,.5);
border:1px solid rgba(194,163,90,.18);border-radius:8px;padding:10px 12px;margin:8px 0 14px;
}
.wg-drawer__search input{flex:1;background:none;border:0;outline:none;color:var(--tb-text);font:inherit;font-size:13px;}
.wg-drawer__search input::placeholder{color:var(--tb-muted);}
.wg-drawer__link{
display:flex;align-items:center;justify-content:space-between;
font-size:15px;color:var(--tb-soft);padding:12px 8px;border-bottom:1px solid rgba(194,163,90,.08);
}
.wg-drawer__link.is-active{color:var(--tb-text);}
.wg-drawer__sub{display:none;padding:2px 8px 6px;}
.wg-drawer__link[aria-expanded="true"] + .wg-drawer__sub{display:block;}
.wg-drawer__sub a{display:block;font-size:13.5px;color:var(--tb-muted);padding:9px 14px;}
.wg-drawer__sub a:hover{color:var(--tb-text);}
.wg-drawer__util{display:flex;align-items:center;gap:18px;margin-top:18px;padding:0 8px;}
.wg-drawer__util a{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--tb-soft);}
.wg-drawer__util .dot{width:7px;height:7px;border-radius:50%;background:var(--tb-gold);}
.wg-drawer__auth{display:flex;gap:12px;margin-top:18px;padding:0 8px;}
.wg-drawer__auth .wg-topbar__register{flex:1;text-align:center;}
.wg-drawer__auth .wg-topbar__login{flex:1;text-align:center;padding:9px 0;border:1px solid rgba(194,163,90,.4);border-radius:6px;}
@media (max-width:1024px){
.wg-topbar__bar{padding:0 16px;height:58px;}
.wg-topbar{--tb-h:58px;}
.wg-topbar__nav,.wg-topbar__icon--search,.wg-topbar__searchbox{display:none;}
.wg-topbar__left{gap:0;flex:1;justify-content:center;}
.wg-topbar__burger{display:flex;}
.wg-topbar__brand{position:absolute;left:50%;transform:translateX(-50%);}
.wg-topbar__name{font-size:12px;letter-spacing:.1em;}
.wg-topbar__name--full{display:none;}
.wg-topbar__name--short{display:inline;}
.wg-topbar__glyph{width:18px;height:18px;}
.wg-topbar__glyph i{width:6px;height:6px;}
.wg-topbar__drawer{display:block;}
.wg-topbar__panel--forums,.wg-topbar__panel--user,.wg-topbar__panel--list{display:none;}
.wg-topbar__right .wg-topbar__icon--chat,.wg-topbar__popout{display:none;}
}
/* preview-only switch */
.wg-topbar-demo{
position:fixed;right:18px;bottom:18px;z-index:5000;
display:flex;gap:6px;align-items:center;padding:6px;border-radius:999px;
background:rgba(18,16,23,.95);border:1px solid rgba(194,163,90,.25);
box-shadow:0 8px 26px rgba(0,0,0,.5);font-family:"Jost",sans-serif;
}
.wg-topbar-demo button{
font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:#b9b2a6;
padding:7px 14px;border-radius:999px;cursor:pointer;transition:.2s;
}
.wg-topbar-demo button.on{background:linear-gradient(180deg,#d8bd76,#a8893f);color:#150f08;font-weight:600;}
</style>
<!-- ============================ TOP BAR ============================ -->
<header class="wg-topbar" data-state="member" data-presence="online">
<div class="wg-topbar__bar">
<!-- left: burger (mobile) + brand + nav -->
<div class="wg-topbar__left">
<button class="wg-topbar__burger" data-wg-burger aria-label="Menu" aria-expanded="false">
<span></span><span></span><span></span>
</button>
<a class="wg-topbar__brand" href="/">
<span class="wg-topbar__glyph"><i></i></span>
<span class="wg-topbar__name wg-topbar__name--full">SHADOWS OVER WESTGATE</span>
<span class="wg-topbar__name wg-topbar__name--short">WESTGATE</span>
</a>
<nav class="wg-topbar__nav">
<a class="wg-topbar__link is-active" href="/news">News</a>
<a class="wg-topbar__link" href="/gallery">Gallery</a>
<a class="wg-topbar__link" href="/wiki">Wiki</a>
<!-- Forums: dropdown trigger. data-wg-menu pairs with the panel's data-wg-panel -->
<button class="wg-topbar__link" data-wg-menu="forums" aria-expanded="false" aria-haspopup="true">
Forums
<svg class="caret" width="9" height="6" viewBox="0 0 9 6" fill="none"><path d="M1 1l3.5 3.5L8 1" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
</button>
<a class="wg-topbar__link" href="/category/84/dev-blog">Dev Blog</a>
</nav>
</div>
<!-- right: search + utilities + (member) avatar OR (guest) auth -->
<div class="wg-topbar__right">
<!-- inline search (desktop) -->
<button class="wg-topbar__icon wg-topbar__icon--search" data-wg-search aria-label="Search">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5" stroke="currentColor" stroke-width="1.4"/><path d="M11 11l3 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
</button>
<form class="wg-topbar__searchbox" action="/search" method="get" role="search">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none"><circle cx="6.5" cy="6.5" r="4.5" stroke="#9a9086" stroke-width="1.3"/><path d="M10 10l3 3" stroke="#9a9086" stroke-width="1.3" stroke-linecap="round"/></svg>
<input type="search" name="term" placeholder="Search Westgate…" data-wg-search-input>
</form>
<!-- ===== MEMBER cluster ===== (live: {{{ if loggedIn }}}) -->
<div class="wg-topbar__authed" data-wg-when="member" style="display:flex;align-items:center;gap:16px;">
<!-- ============================================================
POP-OUTS. Structure mirrors Harmony's sidebar partials:
drafts -> partials/sidebar/drafts.tpl
notifications -> partials/sidebar/notifications.tpl
chats -> partials/sidebar/chats.tpl
In the live theme these ARE those partials (Bootstrap
data-bs-toggle="dropdown"); NodeBB's client JS fills each
[component="…/list"] container at runtime. The rows below are
styled stand-ins so the panels aren't empty in preview.
============================================================ -->
<!-- Drafts (partials/sidebar/drafts.tpl) -->
<div class="wg-topbar__popout">
<button class="wg-topbar__icon" data-wg-menu="drafts" aria-label="Drafts" aria-expanded="false" aria-haspopup="true" title="Drafts">
<svg width="17" height="17" viewBox="0 0 18 18" fill="none"><path d="M10 3H4a1 1 0 00-1 1v10a1 1 0 001 1h10a1 1 0 001-1V8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><path d="M12.5 2.5l3 3-6 6H6.5v-3l6-6z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>
<span class="wg-topbar__badge" data-count="drafts">2</span>
</button>
<div class="wg-topbar__panel wg-topbar__panel--list" data-wg-panel="drafts" role="menu">
<div class="wg-pop__list" data-component="drafts/list">
<!-- each: drafts/open + drafts/delete -->
<div class="wg-draft" data-save-id="1">
<a class="wg-draft__open" href="/category/3/roleplay/compose">
<div class="wg-draft__title">A letter left at the Undergate</div>
<div class="wg-draft__text">My dear Caelum — the arrangement we spoke of cannot wait for the next moon. Meet me where the…</div>
<div class="wg-draft__meta"><span class="wg-cat-label">Roleplay &amp; Stories</span><span class="wg-timeago">2h ago</span></div>
</a>
<button class="wg-draft__del" aria-label="Delete draft" title="Delete draft">
<svg width="13" height="13" viewBox="0 0 14 14" fill="none"><path d="M3 4h8M5.5 4V2.8h3V4M4.2 4l.5 7.2h4.6L9.8 4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
<hr class="wg-pop__sep">
<div class="wg-draft" data-save-id="2">
<a class="wg-draft__open" href="/category/2/general/compose">
<div class="wg-draft__title">Replying to "Best smuggling routes?"</div>
<div class="wg-draft__text">Avoid the Customs Quay after dusk — the new harbourmaster doubled the watch. The cistern tunnels…</div>
<div class="wg-draft__meta"><span class="wg-cat-label">General Discussion</span><span class="wg-timeago">yesterday</span></div>
</a>
<button class="wg-draft__del" aria-label="Delete draft" title="Delete draft">
<svg width="13" height="13" viewBox="0 0 14 14" fill="none"><path d="M3 4h8M5.5 4V2.8h3V4M4.2 4l.5 7.2h4.6L9.8 4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
<!-- empty state (drafts.tpl .no-drafts) -->
<div class="wg-pop__empty" hidden>
<svg width="26" height="26" viewBox="0 0 24 24" fill="none"><path d="M3 12h6l1.5-3 3 6L15 12h6" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>You have no saved drafts.</span>
</div>
</div>
</div>
</div>
<!-- Notifications (partials/sidebar/notifications.tpl) -->
<div class="wg-topbar__popout">
<button class="wg-topbar__icon" data-wg-menu="notifications" aria-label="Notifications" aria-expanded="false" aria-haspopup="true" title="Notifications">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M9 2a4 4 0 00-4 4c0 4-1.5 5-1.5 5h11S13 10 13 6a4 4 0 00-4-4z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M7.5 14a1.5 1.5 0 003 0" stroke="currentColor" stroke-width="1.3"/></svg>
<span class="wg-topbar__badge" data-count="notifications">3</span>
</button>
<div class="wg-topbar__panel wg-topbar__panel--list" data-wg-panel="notifications" role="menu">
<!-- filter tabs (notifications.tpl) -->
<div class="wg-pop__tabs">
<button class="wg-pop__tab is-active" data-filter="all">All</button>
<button class="wg-pop__tab" data-filter="unread">Unread <span class="wg-pop__tabcount">3</span></button>
</div>
<hr class="wg-pop__sep">
<div class="wg-pop__list" data-component="notifications/list">
<a class="wg-notif unread" href="/post/2410">
<span class="wg-ava" style="--c:#5a3d6e;">V</span>
<span class="wg-notif__body">
<span class="wg-notif__text"><strong>Velessa Thorne</strong> replied to your topic <strong>The Masquerade at Hollow House</strong>.</span>
<span class="wg-timeago">12 minutes ago</span>
</span>
<span class="wg-notif__dot" aria-label="unread"></span>
</a>
<hr class="wg-pop__sep">
<a class="wg-notif unread" href="/post/2402">
<span class="wg-ava" style="--c:#6e4a2c;">G</span>
<span class="wg-notif__body">
<span class="wg-notif__text"><strong>Guildmaster Orin</strong> mentioned you in <strong>Contracts &amp; Commissions</strong>.</span>
<span class="wg-timeago">1 hour ago</span>
</span>
<span class="wg-notif__dot" aria-label="unread"></span>
</a>
<hr class="wg-pop__sep">
<a class="wg-notif unread" href="/post/2388">
<span class="wg-ava" style="--c:#3d5a4e;">M</span>
<span class="wg-notif__body">
<span class="wg-notif__text"><strong>Magpie</strong> and 3 others upvoted your post.</span>
<span class="wg-timeago">3 hours ago</span>
</span>
<span class="wg-notif__dot" aria-label="unread"></span>
</a>
<hr class="wg-pop__sep">
<a class="wg-notif" href="/post/2350">
<span class="wg-ava" style="--c:#5a2c2c;">H</span>
<span class="wg-notif__body">
<span class="wg-notif__text"><strong>House Verrault</strong> accepted your application.</span>
<span class="wg-timeago">yesterday</span>
</span>
</a>
</div>
<hr class="wg-pop__sep">
<div class="wg-pop__foot">
<button class="wg-pop__btn"><svg width="13" height="13" viewBox="0 0 14 14" fill="none"><path d="M1 7.5l3 3 5-7M7 10l1.5 1.5 5-7" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg> Mark all read</button>
<a class="wg-pop__btn wg-pop__btn--primary" href="/notifications"><svg width="13" height="13" viewBox="0 0 14 14" fill="none"><path d="M4 3.5h8M4 7h8M4 10.5h8M1.4 3.5h.01M1.4 7h.01M1.4 10.5h.01" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg> See all</a>
</div>
</div>
</div>
<!-- Chat (partials/sidebar/chats.tpl) -->
<div class="wg-topbar__popout wg-topbar__icon--chat">
<button class="wg-topbar__icon" data-wg-menu="chats" aria-label="Messages" aria-expanded="false" aria-haspopup="true" title="Messages">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M3 4h12v8H7l-3 3v-3H3z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>
<span class="wg-topbar__badge" data-count="chats">2</span>
</button>
<div class="wg-topbar__panel wg-topbar__panel--list" data-wg-panel="chats" role="menu">
<div class="wg-pop__list" data-component="chat/list">
<a class="wg-chat unread" href="/chats/12">
<span class="wg-ava" style="--c:#5a3d6e;">V</span>
<span class="wg-chat__body">
<span class="wg-chat__top"><span class="wg-chat__name">Velessa Thorne</span><span class="wg-timeago">2m</span></span>
<span class="wg-chat__last">Bring the ledger. We settle accounts tonight.</span>
</span>
<span class="wg-notif__dot" aria-label="unread"></span>
</a>
<hr class="wg-pop__sep">
<a class="wg-chat unread" href="/chats/9">
<span class="wg-ava" style="--c:#2c4a6e;">T</span>
<span class="wg-chat__body">
<span class="wg-chat__top"><span class="wg-chat__name">The Cinderfall Crew</span><span class="wg-timeago">40m</span></span>
<span class="wg-chat__last"><strong>Rooke:</strong> meet at the third bell, usual place</span>
</span>
<span class="wg-notif__dot" aria-label="unread"></span>
</a>
<hr class="wg-pop__sep">
<a class="wg-chat" href="/chats/4">
<span class="wg-ava" style="--c:#6e4a2c;">O</span>
<span class="wg-chat__body">
<span class="wg-chat__top"><span class="wg-chat__name">Guildmaster Orin</span><span class="wg-timeago">3h</span></span>
<span class="wg-chat__last">Good. The commission is yours.</span>
</span>
</a>
</div>
<hr class="wg-pop__sep">
<div class="wg-pop__foot">
<button class="wg-pop__btn"><svg width="13" height="13" viewBox="0 0 14 14" fill="none"><path d="M1 7.5l3 3 5-7M7 10l1.5 1.5 5-7" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg> Mark all read</button>
<a class="wg-pop__btn wg-pop__btn--primary" href="/user/archvillainette/chats"><svg width="13" height="13" viewBox="0 0 14 14" fill="none"><path d="M4 3.5h8M4 7h8M4 10.5h8M1.4 3.5h.01M1.4 7h.01M1.4 10.5h.01" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg> See all</a>
</div>
</div>
</div>
<!-- user menu -->
<div class="wg-topbar__usermenu">
<button class="wg-topbar__avatar" data-wg-menu="user" aria-label="Account" aria-expanded="false" aria-haspopup="true">
<!-- LIVE: {{{ if user.picture }}}<img src="{user.picture}" alt="{user.username}">{{{ else }}}<span class="wg-topbar__avatar-letter" style="background-color:{user.icon:bgColor}">{user.icon:text}</span>{{{ end }}} -->
<span class="wg-topbar__avatar-letter">A</span>
</button>
</div>
</div>
<!-- ===== GUEST cluster ===== (live: {{{ else }}}) -->
<div class="wg-topbar__guest" data-wg-when="guest" style="display:none;align-items:center;gap:16px;">
<a class="wg-topbar__login" href="/login">Log In</a>
<a class="wg-topbar__register" href="/register">Register</a>
</div>
</div>
</div>
<!-- ===== Forums mega-menu (sample categories — wire to real list) ===== -->
<div class="wg-topbar__panel wg-topbar__panel--forums" data-wg-panel="forums" role="menu">
<div class="wg-mega__grid">
<a class="wg-mega__item" href="/category/1/news"><div class="t">News</div><div class="d">Official server announcements</div></a>
<a class="wg-mega__item" href="/category/84/dev-blog"><div class="t">Developer Blog</div><div class="d">Behind-the-scenes updates</div></a>
<a class="wg-mega__item is-active" href="/category/2/general"><div class="t">General Discussion</div><div class="d">Talk of the city and beyond</div></a>
<a class="wg-mega__item" href="/category/3/roleplay"><div class="t">Roleplay &amp; Stories</div><div class="d">In-character journals and tales</div></a>
<a class="wg-mega__item" href="/category/4/guilds"><div class="t">Guilds &amp; Factions</div><div class="d">Masked guilds and noble houses</div></a>
<a class="wg-mega__item" href="/category/5/help"><div class="t">Help &amp; Support</div><div class="d">New players start here</div></a>
</div>
<div class="wg-mega__foot">
<div class="links">
<a href="/recent">Recent</a>
<a href="/unread">Unread</a>
<a href="/tags">Tags</a>
</div>
<a class="all" href="/categories">All Categories →</a>
</div>
</div>
<!-- ===== User menu ===== (live: Harmony user dropdown include) ===== -->
<div class="wg-topbar__panel wg-topbar__panel--user" data-wg-panel="user" role="menu">
<a class="wg-usermenu__head" href="/user/archvillainette">
<!-- LIVE: {{{ if user.picture }}}<img class="wg-topbar__avatar av" src="{user.picture}" alt="">{{{ else }}}<span class="wg-topbar__avatar av"><span class="wg-topbar__avatar-letter" style="background-color:{user.icon:bgColor}">{user.icon:text}</span></span>{{{ end }}} -->
<span class="wg-topbar__avatar av"><span class="wg-topbar__avatar-letter">A</span></span>
<span style="min-width:0;">
<span class="nm" style="display:block;">archvillainette</span>
<span class="vp" style="display:block;">View profile</span>
</span>
</a>
<div class="wg-usermenu__list">
<!-- Presence (LIVE: Harmony user-status include; sets your online state) -->
<div class="wg-usermenu__label">Status</div>
<button class="wg-usermenu__row presence is-active" data-presence="online">
<span class="dot" style="background:#7fb86a;"></span>Online
<svg class="chk" width="13" height="13" viewBox="0 0 13 13" fill="none"><path d="M2.5 7l3 3 5-6.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button class="wg-usermenu__row presence" data-presence="away">
<span class="dot" style="background:#d8bd76;"></span>Away
</button>
<button class="wg-usermenu__row presence" data-presence="dnd">
<span class="dot" style="background:#bf6a66;"></span>Do not disturb
</button>
<button class="wg-usermenu__row presence" data-presence="offline">
<span class="dot" style="background:#6b6152;"></span>Invisible
</button>
<div class="wg-usermenu__sep"></div>
<a class="wg-usermenu__row" href="/user/archvillainette/bookmarks">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none"><path d="M4 2h7v11l-3.5-2.2L4 13z" stroke="#a8893f" stroke-width="1.3" stroke-linejoin="round"/></svg>
Bookmarks
</a>
<a class="wg-usermenu__row" href="/user/archvillainette/edit">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none"><path d="M9.4 2.8l2.8 2.8M9.8 2.4a1.3 1.3 0 011.8 0l.2.2a1.3 1.3 0 010 1.8L5.3 11.7l-3 .9.9-3 6.6-6.6z" stroke="#a8893f" stroke-width="1.3" stroke-linejoin="round"/></svg>
Edit Profile
</a>
<a class="wg-usermenu__row" href="/user/archvillainette/settings">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none"><circle cx="7.5" cy="7.5" r="2.4" stroke="#a8893f" stroke-width="1.3"/><path d="M7.5 1.5v2M7.5 11.5v2M1.5 7.5h2M11.5 7.5h2" stroke="#a8893f" stroke-width="1.3" stroke-linecap="round"/></svg>
Settings
</a>
<!-- Moderator Tools — privilege-gated server-side ({{{ if privileges.isAdminOrMod }}}) -->
<div class="wg-usermenu__label">Moderator Tools</div>
<a class="wg-usermenu__row" href="/flags">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none"><path d="M3.5 1.8v11.4M3.5 2.4h7.5l-1.5 2.6 1.5 2.6h-7.5" stroke="#a8893f" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
Flagged Content
</a>
<a class="wg-usermenu__row" href="/post-queue">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none"><rect x="2.6" y="2.6" width="9.8" height="9.8" rx="1.4" stroke="#a8893f" stroke-width="1.3"/><path d="M5 5.4h5M5 7.5h5M5 9.6h2.8" stroke="#a8893f" stroke-width="1.3" stroke-linecap="round"/></svg>
Post Queue
</a>
<a class="wg-usermenu__row" href="/ip-blacklist">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none"><circle cx="7.5" cy="7.5" r="5" stroke="#a8893f" stroke-width="1.3"/><path d="M4 4l7 7" stroke="#a8893f" stroke-width="1.3" stroke-linecap="round"/></svg>
IP Blacklist
</a>
<div class="wg-usermenu__sep"></div>
<a class="wg-usermenu__row danger" href="/logout">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none"><path d="M6 2H3v11h3M9.5 4.5L12.5 7.5 9.5 10.5M12 7.5H6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
Logout
</a>
</div>
</div>
<!-- ===== Mobile drawer ===== -->
<div class="wg-topbar__drawer">
<div class="wg-topbar__drawer-in">
<form class="wg-drawer__search" action="/search" method="get" role="search">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none"><circle cx="6.5" cy="6.5" r="4.5" stroke="#9a9086" stroke-width="1.3"/><path d="M10 10l3 3" stroke="#9a9086" stroke-width="1.3" stroke-linecap="round"/></svg>
<input type="search" name="term" placeholder="Search Westgate…">
</form>
<a class="wg-drawer__link is-active" href="/news">News</a>
<a class="wg-drawer__link" href="/gallery">Gallery</a>
<a class="wg-drawer__link" href="/wiki">Wiki</a>
<button class="wg-drawer__link" data-wg-drawer-sub aria-expanded="false">
Forums
<svg width="9" height="6" viewBox="0 0 9 6" fill="none"><path d="M1 1l3.5 3.5L8 1" stroke="#a8893f" stroke-width="1.4" stroke-linecap="round"/></svg>
</button>
<div class="wg-drawer__sub">
<a href="/category/2/general">General Discussion</a>
<a href="/category/3/roleplay">Roleplay &amp; Stories</a>
<a href="/category/4/guilds">Guilds &amp; Factions</a>
<a href="/category/5/help">Help &amp; Support</a>
<a href="/categories">All Categories →</a>
</div>
<a class="wg-drawer__link" href="/category/84/dev-blog">Dev Blog</a>
<!-- member utilities -->
<div class="wg-drawer__util" data-wg-when="member">
<a href="/notifications"><svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M9 2a4 4 0 00-4 4c0 4-1.5 5-1.5 5h11S13 10 13 6a4 4 0 00-4-4z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M7.5 14a1.5 1.5 0 003 0" stroke="currentColor" stroke-width="1.3"/></svg>Notifications<span class="dot"></span></a>
<a href="/chats"><svg width="18" height="18" viewBox="0 0 18 18" fill="none"><path d="M3 4h12v8H7l-3 3v-3H3z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>Messages</a>
</div>
<!-- guest auth -->
<div class="wg-drawer__auth" data-wg-when="guest" style="display:none;">
<a class="wg-topbar__login" href="/login">Log In</a>
<a class="wg-topbar__register" href="/register">Register</a>
</div>
</div>
</div>
</header>
<!-- PREVIEW-ONLY: member/guest switch. Delete this block for production. -->
<div class="wg-topbar-demo">
<button data-wg-demo="member" class="on">Member</button>
<button data-wg-demo="guest">Guest</button>
</div>
<script>
(function(){
var bar = document.querySelector('.wg-topbar');
if(!bar) return;
// ---- dropdown menus (Forums + user) ----
var triggers = bar.querySelectorAll('[data-wg-menu]');
function closeMenus(except){
bar.querySelectorAll('[data-wg-panel]').forEach(function(p){
if(p.getAttribute('data-wg-panel') !== except) p.classList.remove('is-open');
});
triggers.forEach(function(t){
if(t.getAttribute('data-wg-menu') !== except) t.setAttribute('aria-expanded','false');
});
}
triggers.forEach(function(t){
var key = t.getAttribute('data-wg-menu');
var panel = bar.querySelector('[data-wg-panel="'+key+'"]');
t.addEventListener('click', function(e){
e.stopPropagation();
var open = panel.classList.contains('is-open');
closeMenus(open ? null : key);
panel.classList.toggle('is-open', !open);
t.setAttribute('aria-expanded', String(!open));
});
});
// keep panel open when interacting inside it
bar.querySelectorAll('[data-wg-panel]').forEach(function(p){
p.addEventListener('click', function(e){ e.stopPropagation(); });
});
// ---- inline search ----
var searchBtn = bar.querySelector('[data-wg-search]');
var searchInput = bar.querySelector('[data-wg-search-input]');
if(searchBtn){
searchBtn.addEventListener('click', function(e){
e.stopPropagation();
bar.classList.add('is-searching');
closeMenus(null);
if(searchInput) setTimeout(function(){ searchInput.focus(); }, 60);
});
}
function closeSearch(){
if(searchInput && searchInput.value.trim() !== '') return; // keep if user typed
bar.classList.remove('is-searching');
}
// ---- mobile burger + drawer ----
var burger = bar.querySelector('[data-wg-burger]');
if(burger){
burger.addEventListener('click', function(e){
e.stopPropagation();
var open = bar.classList.toggle('is-drawer');
burger.setAttribute('aria-expanded', String(open));
});
}
// drawer Forums accordion
var drawerSub = bar.querySelector('[data-wg-drawer-sub]');
if(drawerSub){
drawerSub.addEventListener('click', function(){
var open = drawerSub.getAttribute('aria-expanded') === 'true';
drawerSub.setAttribute('aria-expanded', String(!open));
});
}
// ---- presence / status selector (drives the avatar ring; persists locally) ----
var checkSvg = '<svg class="chk" width="13" height="13" viewBox="0 0 13 13" fill="none"><path d="M2.5 7l3 3 5-6.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
function applyPresence(key){
var row = bar.querySelector('[data-presence="' + key + '"]');
if(!row) return;
bar.setAttribute('data-presence', key); // switches --tb-presence ring colour
bar.querySelectorAll('[data-presence]').forEach(function(x){
x.classList.remove('is-active');
var c = x.querySelector('.chk'); if(c) c.remove();
});
row.classList.add('is-active');
if(!row.querySelector('.chk')) row.insertAdjacentHTML('beforeend', checkSvg);
}
bar.querySelectorAll('[data-presence]').forEach(function(p){
p.addEventListener('click', function(e){
e.stopPropagation();
var key = p.getAttribute('data-presence');
applyPresence(key);
try { localStorage.setItem('wg-topbar-presence', key); } catch(_){}
});
});
// restore last choice so it sticks across reloads (live: read NodeBB user status)
try {
var saved = localStorage.getItem('wg-topbar-presence');
if(saved) applyPresence(saved);
} catch(_){}
// ---- pop-out niceties (preview-only; live these are NodeBB component actions) ----
// notification filter tabs (data-filter all/unread)
bar.querySelectorAll('.wg-pop__tab').forEach(function(tab){
tab.addEventListener('click', function(e){
e.stopPropagation();
var tabs = tab.parentElement.querySelectorAll('.wg-pop__tab');
tabs.forEach(function(t){ t.classList.remove('is-active'); });
tab.classList.add('is-active');
var unreadOnly = tab.getAttribute('data-filter') === 'unread';
var list = tab.closest('[data-wg-panel]').querySelector('[data-component="notifications/list"]');
list.querySelectorAll('.wg-notif').forEach(function(row){
var hide = unreadOnly && !row.classList.contains('unread');
row.style.display = hide ? 'none' : '';
var sep = row.nextElementSibling;
if(sep && sep.classList.contains('wg-pop__sep')) sep.style.display = hide ? 'none' : '';
});
});
});
// mark-all-read: clear unread rows + dots + badge for that panel
bar.querySelectorAll('.wg-pop__btn:not(.wg-pop__btn--primary)').forEach(function(btn){
btn.addEventListener('click', function(e){
e.stopPropagation();
var panel = btn.closest('[data-wg-panel]');
panel.querySelectorAll('.unread').forEach(function(r){ r.classList.remove('unread'); });
panel.querySelectorAll('.wg-notif__dot').forEach(function(d){ d.remove(); });
var key = panel.getAttribute('data-wg-panel');
var badge = bar.querySelector('.wg-topbar__badge[data-count="'+key+'"]');
if(badge) badge.style.display = 'none';
var tabc = panel.querySelector('.wg-pop__tabcount'); if(tabc) tabc.textContent = '0';
});
});
// draft delete: drop the row (+ its separator)
bar.querySelectorAll('.wg-draft__del').forEach(function(del){
del.addEventListener('click', function(e){
e.preventDefault(); e.stopPropagation();
var row = del.closest('.wg-draft');
var sep = row.nextElementSibling || row.previousElementSibling;
if(sep && sep.classList && sep.classList.contains('wg-pop__sep')) sep.remove();
row.remove();
var list = bar.querySelector('[data-component="drafts/list"]');
var badge = bar.querySelector('.wg-topbar__badge[data-count="drafts"]');
var left = list.querySelectorAll('.wg-draft').length;
if(badge) badge.textContent = left;
if(left === 0){
if(badge) badge.style.display = 'none';
var empty = list.querySelector('.wg-pop__empty'); if(empty) empty.hidden = false;
}
});
});
// ---- outside click + Escape close everything ----
document.addEventListener('click', function(){ closeMenus(null); closeSearch(); });
document.addEventListener('keydown', function(e){
if(e.key === 'Escape'){
closeMenus(null);
bar.classList.remove('is-searching');
bar.classList.remove('is-drawer');
if(burger) burger.setAttribute('aria-expanded','false');
}
});
// ---- PREVIEW-ONLY: member/guest switch ----
var demo = document.querySelectorAll('[data-wg-demo]');
function setState(state){
bar.setAttribute('data-state', state);
document.querySelectorAll('[data-wg-when]').forEach(function(el){
var match = el.getAttribute('data-wg-when') === state;
el.style.display = match ? 'flex' : 'none';
});
demo.forEach(function(b){ b.classList.toggle('on', b.getAttribute('data-wg-demo') === state); });
closeMenus(null);
}
demo.forEach(function(b){
b.addEventListener('click', function(){ setState(b.getAttribute('data-wg-demo')); });
});
})();
</script>
@@ -0,0 +1,223 @@
/* ============================================================================
SHADOWS OVER WESTGATE top bar controller
----------------------------------------------------------------------------
Paste into ACP -> Settings -> Custom Content -> "Custom JavaScript".
(In the live theme this can instead live in the theme's client-side bundle.)
WHY THIS FILE EXISTS
- The two static pages (home.html / join-the-team.html) need NO JavaScript;
they are pure HTML/CSS (the only inline bit is an <img onerror> fallback).
- The TOP BAR needs this small controller for: Forums mega-menu, inline
search, the mobile burger/drawer, and the drawer's Forums accordion.
NODEBB-SPECIFIC NOTES
- NodeBB is a single-page app: it swaps #content on navigation instead of
reloading. So we DON'T bind to specific nodes on DOMContentLoaded we use
event delegation on `document`, which keeps working across page changes and
even if the header is re-rendered.
- Inline <script> inside an HTML widget is sanitised/unreliable, which is the
other reason the logic lives here rather than in the pasted markup.
- The live notification / chat / user dropdowns come from Harmony and carry
their OWN JavaScript. If you keep those, you do NOT need this script to open
them only the Forums menu, search, and mobile drawer below.
- The preview-only member/guest switch from top-bar.html is intentionally
omitted; in production the cluster is chosen server-side ({{{ if loggedIn }}}).
========================================================================== */
(function () {
'use strict';
function bar() { return document.querySelector('.wg-topbar'); }
function closeMenus(exceptKey) {
var b = bar();
if (!b) return;
b.querySelectorAll('[data-wg-panel]').forEach(function (p) {
if (p.getAttribute('data-wg-panel') !== exceptKey) p.classList.remove('is-open');
});
b.querySelectorAll('[data-wg-menu]').forEach(function (t) {
if (t.getAttribute('data-wg-menu') !== exceptKey) t.setAttribute('aria-expanded', 'false');
});
}
function closeSearch() {
var b = bar();
if (!b) return;
var input = b.querySelector('[data-wg-search-input]');
if (input && input.value.trim() !== '') return; // keep open if the user typed
b.classList.remove('is-searching');
}
var CHECK_SVG = '<svg class="chk" width="13" height="13" viewBox="0 0 13 13" fill="none"><path d="M2.5 7l3 3 5-6.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
function applyPresence(key) {
var b = bar();
if (!b) return;
var row = b.querySelector('[data-presence="' + key + '"]');
if (!row) return;
b.setAttribute('data-presence', key); // switches the --tb-presence avatar ring
b.querySelectorAll('[data-presence]').forEach(function (x) {
x.classList.remove('is-active');
var c = x.querySelector('.chk'); if (c) c.remove();
});
row.classList.add('is-active');
if (!row.querySelector('.chk')) row.insertAdjacentHTML('beforeend', CHECK_SVG);
}
function restorePresence() {
try {
var saved = localStorage.getItem('wg-topbar-presence');
if (saved) applyPresence(saved);
} catch (_) {}
}
// ---- one delegated click handler for the whole bar ----
document.addEventListener('click', function (e) {
var b = bar();
if (!b) return;
// dropdown trigger (Forums / and any future data-wg-menu)
var trigger = e.target.closest && e.target.closest('[data-wg-menu]');
if (trigger && b.contains(trigger)) {
e.stopPropagation();
var key = trigger.getAttribute('data-wg-menu');
var panel = b.querySelector('[data-wg-panel="' + key + '"]');
var open = panel && panel.classList.contains('is-open');
closeMenus(open ? null : key);
if (panel) panel.classList.toggle('is-open', !open);
trigger.setAttribute('aria-expanded', String(!open));
return;
}
// clicks inside an open panel shouldn't close it
if (e.target.closest && e.target.closest('[data-wg-panel]')) {
e.stopPropagation();
return;
}
// search toggle
var searchBtn = e.target.closest && e.target.closest('[data-wg-search]');
if (searchBtn && b.contains(searchBtn)) {
e.stopPropagation();
b.classList.add('is-searching');
closeMenus(null);
var input = b.querySelector('[data-wg-search-input]');
if (input) setTimeout(function () { input.focus(); }, 60);
return;
}
// mobile burger
var burger = e.target.closest && e.target.closest('[data-wg-burger]');
if (burger && b.contains(burger)) {
e.stopPropagation();
var drawerOpen = b.classList.toggle('is-drawer');
burger.setAttribute('aria-expanded', String(drawerOpen));
return;
}
// drawer Forums accordion
var sub = e.target.closest && e.target.closest('[data-wg-drawer-sub]');
if (sub && b.contains(sub)) {
var subOpen = sub.getAttribute('aria-expanded') === 'true';
sub.setAttribute('aria-expanded', String(!subOpen));
return;
}
// presence / status selector (drives the avatar ring). LIVE: also POST the
// new status to NodeBB (e.g. socket.emit('user.setStatus', ...)) so it
// persists server-side; here it persists in localStorage so it sticks.
var presence = e.target.closest && e.target.closest('[data-presence]');
if (presence && b.contains(presence)) {
e.stopPropagation();
var key = presence.getAttribute('data-presence');
applyPresence(key);
try { localStorage.setItem('wg-topbar-presence', key); } catch (_) {}
return;
}
// ----- pop-out actions -----
// NOTE: in the LIVE theme the pop-outs ARE Harmony's partials
// (drafts.tpl / notifications.tpl / chats.tpl), and these actions are
// handled by NodeBB core JS (component="notifications/list", drafts/delete,
// chats/mark-all-read, etc). The handlers below only drive THIS preview's
// static rows — drop them when wiring the real includes.
// notification filter tabs
var tab = e.target.closest && e.target.closest('.wg-pop__tab');
if (tab && b.contains(tab)) {
e.stopPropagation();
tab.parentElement.querySelectorAll('.wg-pop__tab').forEach(function (t) { t.classList.remove('is-active'); });
tab.classList.add('is-active');
var unreadOnly = tab.getAttribute('data-filter') === 'unread';
var nlist = tab.closest('[data-wg-panel]').querySelector('[data-component="notifications/list"]');
nlist.querySelectorAll('.wg-notif').forEach(function (row) {
var hide = unreadOnly && !row.classList.contains('unread');
row.style.display = hide ? 'none' : '';
var sep = row.nextElementSibling;
if (sep && sep.classList.contains('wg-pop__sep')) sep.style.display = hide ? 'none' : '';
});
return;
}
// mark-all-read
var markBtn = e.target.closest && e.target.closest('.wg-pop__btn:not(.wg-pop__btn--primary)');
if (markBtn && b.contains(markBtn)) {
e.stopPropagation();
var mPanel = markBtn.closest('[data-wg-panel]');
mPanel.querySelectorAll('.unread').forEach(function (r) { r.classList.remove('unread'); });
mPanel.querySelectorAll('.wg-notif__dot').forEach(function (d) { d.remove(); });
var mKey = mPanel.getAttribute('data-wg-panel');
var mBadge = b.querySelector('.wg-topbar__badge[data-count="' + mKey + '"]');
if (mBadge) mBadge.style.display = 'none';
var tc = mPanel.querySelector('.wg-pop__tabcount'); if (tc) tc.textContent = '0';
return;
}
// draft delete
var del = e.target.closest && e.target.closest('.wg-draft__del');
if (del && b.contains(del)) {
e.preventDefault(); e.stopPropagation();
var dRow = del.closest('.wg-draft');
var dSep = dRow.nextElementSibling || dRow.previousElementSibling;
if (dSep && dSep.classList && dSep.classList.contains('wg-pop__sep')) dSep.remove();
dRow.remove();
var dList = b.querySelector('[data-component="drafts/list"]');
var dBadge = b.querySelector('.wg-topbar__badge[data-count="drafts"]');
var left = dList.querySelectorAll('.wg-draft').length;
if (dBadge) dBadge.textContent = left;
if (left === 0) {
if (dBadge) dBadge.style.display = 'none';
var empty = dList.querySelector('.wg-pop__empty'); if (empty) empty.hidden = false;
}
return;
}
// outside click: close everything that floats
closeMenus(null);
closeSearch();
});
// ---- Escape closes everything ----
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
var b = bar();
if (!b) return;
closeMenus(null);
b.classList.remove('is-searching');
b.classList.remove('is-drawer');
var burger = b.querySelector('[data-wg-burger]');
if (burger) burger.setAttribute('aria-expanded', 'false');
});
// ---- close the mobile drawer after an in-app navigation ----
if (window.$ && typeof $.fn === 'object') {
$(window).on('action:ajaxify.end', function () {
var b = bar();
if (!b) return;
b.classList.remove('is-drawer', 'is-searching');
closeMenus(null);
restorePresence();
});
}
// ---- restore the saved status on first load ----
if (document.readyState !== 'loading') restorePresence();
else document.addEventListener('DOMContentLoaded', restorePresence);
})();
@@ -0,0 +1,158 @@
# Global Topbar Design Spec
Date: 2026-06-26
Status: draft, goal definition
## Goal
Complete the Westgate global topbar as the theme-owned replacement for
Harmony's NodeBB sidebars.
The current `custom_pages/westgate-pages/top-bar.html` and
`custom_pages/westgate-pages/topbar-custom.js` files are reference material from
the design extraction. They show the intended visual language and rough
interaction shape, but they are not production behavior. The finished topbar
belongs in this theme as global chrome, not as ACP Custom Content pasted onto
individual pages.
The topbar must make website pages, wiki pages, and forum pages feel like one
Westgate application while preserving real NodeBB account, navigation, search,
notification, chat, draft, and status behavior.
## Evidence Checked
- `docs/superpowers/specs/2026-06-26-website-pages-design.md` already defines
the topbar as the owner of Harmony sidebar functions.
- `custom_pages/westgate-pages/THEME-INTEGRATION.md` recommends a
`templates/partials/header/topbar.tpl` theme partial and reuse of Harmony live
components.
- `custom_pages/westgate-pages/top-bar.html` is a visual/reference draft with
placeholder avatar, notification, chat, and draft data.
- `custom_pages/westgate-pages/topbar-custom.js` is a paste-ready preview
controller for ACP Custom JavaScript, with comments noting where live NodeBB
behavior still needs to replace preview behavior.
- `templates/header.tpl` currently imports `partials/sidebar-left.tpl` and lays
out `#panel` beside the sidebar.
- Harmony still supplies the inherited right sidebar through its footer unless
Westgate overrides that path.
- `theme.scss` is imports-only and already imports focused files from
`scss/westgate/`.
## Required Behavior
The Westgate topbar replaces the NodeBB sidebar experience. It must own every
user-facing button or action that currently lives in Harmony's desktop sidebars
or duplicate mobile bars:
- Brand/home entry.
- ACP Navigation links and active states.
- Forum/category entry points.
- Search, including NodeBB quick search behavior.
- Notifications, with real unread counts and notification list behavior.
- Chats when chat is available to the user.
- Drafts, with real draft count, open, and delete behavior.
- User avatar menu using the logged-in user's real avatar.
- User status controls that set the user's actual NodeBB status, including
online, away, and invisible when supported by the running NodeBB version.
- Profile, bookmarks, edit profile, settings, moderator/admin affordances, and
logout.
- Guest login and register actions.
- Mobile equivalents for the same primary actions.
The topbar is not allowed to pretend that live data exists. If the current user
has no notifications, chats, or drafts, the topbar must show the real empty or
hidden state. Counts must come from NodeBB data, not hard-coded badge text.
## Visual Direction
Use the draft topbar as a visual target, not as final source. The production
topbar should keep the black velvet, near-black plum, muted gold, restrained
contrast, and small red state detail direction already established by the
Westgate theme.
All substantive styling belongs in a focused SCSS partial such as
`scss/westgate/_topbar.scss`, imported from `theme.scss`. The partial should use
existing `--wg-*` tokens where possible and should not introduce a second visual
system.
Harmony sidebar buttons should be restyled as topbar controls. They should feel
native to the topbar, not like sidebar markup awkwardly placed in a horizontal
row.
## Implementation Direction
The production implementation should be theme-first:
- Add a focused topbar partial, likely
`templates/partials/header/topbar.tpl`.
- Mount the topbar from `templates/header.tpl`.
- Remove the global Harmony left sidebar from the main layout.
- Override the Harmony footer path if needed so the right sidebar no longer
renders as a separate global sidebar.
- Let `#panel` become the normal main content column under the topbar.
- Keep `theme.scss` imports-only.
- Put topbar CSS under `scss/westgate/`.
- Put topbar client behavior in the theme client bundle unless an ACP bridge is
explicitly chosen as a temporary deployment step.
Where possible, reuse Harmony or NodeBB partials and component hooks for live
behavior instead of rebuilding dynamic systems. Important hooks include search,
notifications, chat, drafts, user controls, and status controls. If preserving
Harmony selectors would produce brittle markup, update the theme JavaScript
intentionally and document the changed selector contract.
## Reference Files
The reference files may be copied from selectively, but they should not be
treated as production source:
- `custom_pages/westgate-pages/top-bar.html`: visual structure, states, and
scoped CSS ideas.
- `custom_pages/westgate-pages/topbar-custom.js`: preview interaction patterns
for menus, search expansion, mobile drawer, and escape/outside-click handling.
- `custom_pages/Westgate Top Bar.dc.html`: original visual design export.
Preview-only behavior to remove or replace includes hard-coded notifications,
hard-coded chats, hard-coded drafts, fake status persistence in `localStorage`,
demo member/guest switching, and placeholder avatar rendering.
## Risks
Harmony JavaScript currently assumes sidebar-oriented selectors for search,
drafts, notification/chat counts, sidebar toggles, tooltips, and layout offsets.
The topbar implementation must account for those assumptions directly. A
visually correct topbar is not complete if NodeBB's live sidebar behavior stops
working.
The mobile experience also needs an explicit decision during implementation:
once the Westgate topbar drawer owns the same actions, Harmony's inherited
mobile bars should not duplicate those controls.
## Acceptance Criteria
- No Harmony left or right global sidebar is visible on normal Westgate pages.
- All former sidebar actions remain reachable from the topbar or its mobile
drawer.
- Logged-out users see real login/register actions.
- Logged-in users see their real avatar and the correct account menu.
- Notifications, chats, and drafts use real NodeBB counts and lists.
- Empty notification, chat, and draft states are real states, not mock rows.
- User status buttons call the real NodeBB status behavior and reflect the
resulting state.
- Search uses NodeBB search behavior rather than a visual-only input.
- The topbar appears consistently on Custom Pages, wiki routes, and normal forum
routes.
- The topbar styling follows Westgate theme tokens and lives under
`scss/westgate/`.
- The implementation remains a child theme over `nodebb-theme-harmony`.
## Decisions For Implementation Planning
- Whether to temporarily ship any part of the topbar through ACP Custom Content
for visual validation before moving it fully into the theme.
- Whether the skin switcher remains exposed, and if so where it belongs in the
topbar or account menu.
- Whether legacy `sidebar-footer` widget content is deprecated, moved to a real
footer, or exposed through another intentional surface.
- Which Harmony selectors are preserved for compatibility and which are replaced
with Westgate topbar selectors.
@@ -0,0 +1,546 @@
# Westgate Website Pages Design Spec
Date: 2026-06-26
Status: draft, investigation only
## Goal
Build the public website pages for Shadows Over Westgate inside NodeBB. The site
uses NodeBB as the application platform, the Custom Pages plugin for website
routes/pages, the Westgate theme for shared visual language and global chrome,
and local plugins where Custom Pages needs dynamic data or behavior.
The site should feel like one Westgate experience rather than a forum with
separate pasted pages. The visual target remains black velvet, near-black plum,
muted gold, restraint, decay, and rich contrast. Red is reserved for state or
danger details. Color changes should happen through existing `--wg-*` theme
tokens where possible.
The custom pages to design are:
- Home
- Gallery
- News
- Blog / Developer Blog
- Recruitment
Everything built for these pages belongs under a tracked `custom/` folder.
Everything currently in `custom_pages/` is reference material from the design
extraction and should be pulled from selectively, not treated as production
source.
The major structural goal is that Harmony's left and right sidebar functions are
owned by a global Westgate topbar. Website pages, wiki pages, and forum pages
should share the same navigation, account actions, search, notifications, chat,
drafts, and authentication entry points.
## Evidence Checked
- `theme.json` declares `baseTheme: "nodebb-theme-harmony"`.
- `theme.scss` imports Harmony first, then focused Westgate partials under
`scss/westgate/`.
- `templates/header.tpl` currently matches the Harmony layout pattern: a
`layout-container`, `partials/sidebar-left.tpl`, `#panel`, and
`partials/header/brand.tpl`.
- Harmony's `footer.tpl` imports `partials/sidebar-right.tpl`.
- Harmony's sidebar partials under
`/home/vicky/Repositories/nodebb-theme-harmony/templates/partials/sidebar/`
own the live controls for navigation, user menu, search, notifications, chat,
and drafts.
- Harmony's `public/harmony.js` binds behavior to sidebar-oriented selectors,
especially `[component="sidebar/search"]`, `[component="sidebar/drafts"]`,
`[component="sidebar/toggle"]`, `nav.sidebar [component="notifications/count"]`,
and `nav.sidebar [component="chat/count"]`.
- The wiki plugin at
`/home/vicky/Projects/westgate/repositories/migration/sow-nodebb-plugin-wiki`
is a NodeBB plugin that treats selected categories as wiki namespaces and
topics as wiki pages. Its README says the active theme supplies brand colors,
typography, and panel chrome through CSS custom properties.
- `custom_pages/` contains raw Claude Design exports, screenshots, preview
harnesses, and cleaned paste-ready modules.
- `custom_pages/westgate-pages/THEME-INTEGRATION.md` already sketches a theme
integration path for News, Dev Blog, Gallery, and Topbar.
## Artifact Inventory
### Raw Claude Design Exports
These are reference artifacts, not direct production inputs:
- `custom_pages/Westgate.dc.html`
- `custom_pages/Westgate v1 (approx palette).dc.html`
- `custom_pages/Westgate Top Bar.dc.html`
- `custom_pages/support.js`
- `custom_pages/image-slot.js`
The `.dc.html` files use Claude Design/Omelette style markup such as `<x-dc>`,
`<sc-if>`, inline preview handlers, and runtime helpers. They establish layout,
states, palette exploration, and interaction ideas. They should not be pasted
directly into NodeBB Custom Pages as production page content.
### Reference Custom Pages-Compatible Page Modules
These are cleaned design extraction files that may be used as references while
building tracked production files under `custom/`:
- `custom_pages/westgate-pages/home.html`
- `custom_pages/westgate-pages/join-the-team.html`
- `custom_pages/westgate-pages/top-bar.html`
- `custom_pages/westgate-pages/topbar-custom.js`
`home.html` and `join-the-team.html` are useful because they are already pure
HTML/CSS widgets scoped under `.wg-page`. The production versions should be
rebuilt as `custom/pages/home.html` and `custom/pages/recruitment.html`, with
content and links adjusted for the final routes.
`top-bar.html` is a faithful preview/reference for the global topbar, but it is
not the preferred long-term deployment form. The live topbar belongs in the
theme as a global partial and stylesheet, with client behavior in the theme
client bundle or ACP custom JavaScript only as a temporary bridge.
### Preview Harnesses And Screenshots
Preview harnesses:
- `custom_pages/westgate-pages/_preview-home.html`
- `custom_pages/westgate-pages/_preview-join.html`
- `custom_pages/westgate-pages/_preview-topbar.html`
Screenshots under `custom_pages/screenshots/` capture the visual target and
states. They are useful for QA and comparison, but should not be treated as
source assets for the live site unless explicitly selected.
## Repository Ownership Model
### `custom/` Is The Production Custom Pages Source
All custom-page artifacts we build should live under `custom/` in this
repository. The first layout should be:
```text
custom/
pages/
home.html
gallery.html
news.html
blog.html
recruitment.html
shared/
assets/
```
The `custom/pages/*.html` files are the source files intended to be installed
through NodeBB Custom Pages and Widgets. They may start as paste-ready HTML/CSS,
but they should be treated as source we own, not generated exports.
`custom/shared/` is reserved for shared snippets, notes, or small page fragments
that are intentionally reused across the five pages. It should stay small; if a
shared concern becomes real application behavior, move it into the theme or a
plugin instead.
`custom/assets/` is reserved for manifest notes and selected source assets for
custom-page use. It should not become a dumping ground for generated screenshots
or raw uploads. Actual public media should normally be uploaded through NodeBB or
served by the owning plugin/theme static directory.
### `custom_pages/` Is Reference Only
`custom_pages/` is an extraction/reference folder. It may contain Claude Design
exports, preview harnesses, uploads, screenshots, and exploratory files.
Rules:
- Do not install files directly from `custom_pages/` without first moving or
rewriting the relevant element into `custom/`.
- Do not treat `.dc.html` files as production source.
- Do not track generated previews/screenshots as production assets unless they
are explicitly chosen as QA references.
- When a useful element is pulled from `custom_pages/`, document that provenance
in the relevant `custom/pages/*.html` comment or in this spec.
### Theme Owns Shared Visual Language
The theme remains responsible for the design system and global chrome:
- `--wg-*` design tokens
- fonts
- panel, button, form, card, prose, and widget styling
- topbar
- forum page styling
- wiki parity hooks through `--wiki-*` variables and shared classes
Custom pages should consume the theme's tokens and classes. They should not fork
the palette or create a second visual system.
### Plugins Own Dynamic Behavior When Custom Pages Is Not Enough
NodeBB should do everything, but not every page can be static HTML.
If Custom Pages cannot server-render or safely fetch the data a page needs, a
small local NodeBB plugin should provide the data/API/view-model layer while the
page remains a Custom Pages route. This applies especially to:
- News from forum category `1`
- Blog from forum category `84`
- Gallery if it needs a special upload directory, moderation flow, or asset API
- Future recruitment applications, because no application plugin exists yet
The rule is: Custom Pages owns the website route and page composition; forum
topics/categories remain canonical content where appropriate; plugins may expose
normalized data so the page does not look or behave like a raw forum view.
## Page Ownership
### Static Custom Pages
Use Custom Pages and widgets directly for pages whose content is mostly
editorial and does not require live NodeBB data:
- Home
- Recruitment
Rules:
- Scope page styles under `.wg-page` or a more specific page root such as
`.wg-home` or `.wg-recruit`.
- Use existing `--wg-*` CSS variables for fonts, color, borders, shadows, and
panels.
- Keep inline JavaScript out of static page widgets unless there is a strong
reason. The current reference Home and Recruitment pages require no
JavaScript.
- Treat placeholder upload paths such as `/assets/uploads/westgate/...` as
install-time inputs.
- Leave per-page sidebar widget areas empty unless a page explicitly needs a
sidebar module.
### Dynamic Custom Pages
Use Custom Pages plus plugin-provided data for pages that need live forum or
asset data:
- News
- Blog / Developer Blog
- Gallery
Rules:
- Custom Pages owns the public route.
- The page should present as a website page, not a category or topic template.
- The underlying NodeBB source should remain available for permissions,
moderation, authorship, search, and discussion behavior.
- Data should be normalized before rendering: title, slug, category, author,
date, teaser, body, optional banner image, and canonical discussion link.
- Forum affordances should be hidden from the main website presentation unless
they are intentionally exposed as secondary actions.
### Forum And Wiki Parity
Forums and the wiki are not separate visual products. They must stay in parity
with custom pages:
- Same topbar and global navigation.
- Same `--wg-*` palette, typography, border, panel, and focus treatment.
- Same prose vocabulary for article-like content where practical.
- Same button and card language.
- Same mobile behavior expectations.
- Same accessibility bar: readable contrast, visible focus states, usable hover
and active states.
The wiki plugin already exposes theme hooks through `--wiki-*` CSS variables and
uses `.westgate-wiki` / `.wiki-article-prose` classes. The Westgate theme should
remain the place where those variables are mapped to the `--wg-*` design system.
## Page Specifications
### Home
Production source: `custom/pages/home.html`
Reference source: `custom_pages/westgate-pages/home.html`
Deployment: Custom Pages route `home` or `/`, with the full file pasted into
an HTML widget in the main content area.
Current sections:
- Hero with title, setting line, key-art placeholder, and two CTAs.
- Server status strip with static placeholder data.
- Setting pitch with long-form atmosphere copy.
- Four "Enter the City" step cards.
- Latest News strip with three static card placeholders.
Open data decisions:
- Final route: `/`, `/home`, or both via redirect.
- Final art upload paths.
- Whether server status and player counts remain static at launch or come from a
status widget/API.
- Whether the Latest News strip remains manually curated or is replaced by live
category data later.
### Recruitment
Production source: `custom/pages/recruitment.html`
Reference source: `custom_pages/westgate-pages/join-the-team.html`
Deployment: Custom Pages route `recruitment`, with the full file pasted into
an HTML widget in the main content area.
Current sections:
- Recruitment hero with screenshot placeholder.
- Three contributor pillars.
- Team culture panel.
- CTA linking to an applications page and Discord.
Intent:
- Explain the benefits of building with Westgate.
- Keep the pitch brief.
- Link to an applications page when that exists.
- Link to Discord immediately.
- Do not imply a full application capability exists yet.
Open data decisions:
- Final application route or placeholder route.
- Final Discord invite URL.
- Final art upload paths.
- Whether role categories stay broad or become specific to current recruiting
needs.
- Whether any old `join`, `join-the-team`, or recruitment-adjacent route should
redirect to `recruitment`.
### News And Dev Blog
Production source:
- `custom/pages/news.html`
- `custom/pages/blog.html`
Reference source: `custom_pages/westgate-pages/THEME-INTEGRATION.md`
Deployment: Custom Pages routes backed by NodeBB topic/category data.
Intent:
- News and Blog should be live NodeBB content, not duplicated static pages.
- The page index should render article cards.
- Individual topics in those categories should receive article-style treatment:
banner hero, metadata, title, author, and prose body.
- Users should not need to know the article came from a forum post.
- Discussion should remain available through a secondary link or article footer,
not as the primary presentation.
- Each article can optionally use a banner image pulled from the topic.
Canonical source categories:
- News: `cid 1`
- Blog / Developer Blog: `cid 84`
Known Blog route: `https://westgate.pw/category/84/developer-blog`
Rendering requirements:
- Index pages show cards with title, date, teaser, category label, optional
thumbnail/banner, and author.
- Article pages show an optional banner hero, title, metadata, author, and
article body.
- Article body should reuse the wiki/article prose vocabulary where possible so
News, Blog, Wiki, and forum article content stay visually aligned.
- Missing banners must degrade to a handsome text-led hero, not a broken or empty
media slot.
- The implementation should avoid exposing raw forum chrome in the main article
reading experience.
Open data decisions:
- Whether the route names are `/news` and `/blog`, or whether `/developer-blog`
also exists as a canonical or redirect route.
- How a topic declares its banner image: topic thumbnail, first image, upload
metadata, tag, or a small plugin-owned convention.
- Whether News and Blog share one rendering module with different category IDs.
### Gallery
Production source: `custom/pages/gallery.html`
Reference source: `custom_pages/westgate-pages/THEME-INTEGRATION.md`
Deployment: Custom Pages route backed by gallery asset/topic data.
Intent:
- Gallery showcases screenshots and/or art uploaded specifically for Gallery.
- The source may be forum topics, a special directory, NodeBB uploads, or a
plugin-managed asset list.
- Each item should render as a visual tile with enough metadata to feel curated.
- Category view renders as a responsive masonry-style grid.
- Topic pages remain normal unless later design work requires a custom viewer.
Open data decisions:
- Whether Gallery uses a forum category, a special upload directory, or a
plugin-owned asset registry.
- Upload and moderation workflow.
- Whether Gallery items link to topics, media lightboxes, or standalone custom
pages.
- Required metadata: title, author, date, caption, tags, source topic, or
approval state.
### Forums And Wiki
Forums should remain normal NodeBB/Harmony-derived forum pages styled by
Westgate theme overrides.
Wiki uses the custom Westgate wiki plugin, not Custom Pages. It still must stay
visually aligned with the custom pages and forums. The existing theme already
maps wiki styling through `scss/westgate/_wiki-prose.scss`, and the wiki plugin
ships layout defaults plus `--wiki-*` hooks.
Parity requirements:
- Custom page article prose and wiki article prose should converge where
possible.
- News/Blog article bodies should be close enough to wiki article bodies that a
reader sees one editorial system.
- Forum cards, wiki cards, and custom-page cards should share radius, border,
shadow, type scale, and focus behavior.
- Topbar behavior must be uniform across Custom Pages, wiki routes, and forum
routes.
## Topbar And Harmony Sidebar Migration
### Current Harmony Baseline
Harmony splits global chrome across:
- Left sidebar: ACP Navigation, dropdown navigation items, skin switcher, sidebar
toggle.
- Right sidebar: logged-in user menu, search, notifications, chats, drafts.
- Mobile bottom/top bar: mobile access to related sidebar controls.
Westgate currently inherits this model because `templates/header.tpl` imports
`partials/sidebar-left.tpl` and Harmony `footer.tpl` imports
`partials/sidebar-right.tpl`.
### Required End State
The global topbar owns the functions users previously reached through Harmony's
sidebars:
- Brand link.
- ACP Navigation items.
- Forums menu / category entry points.
- Search.
- Notifications.
- Chats, when `canChat` is true.
- Drafts.
- User avatar menu.
- User status controls.
- Profile, bookmarks, edit profile, settings.
- Moderator tools when available.
- Logout.
- Guest login and register actions.
- Mobile drawer equivalents for the same primary actions.
- Skin switcher if custom skins remain enabled.
The global layout should no longer render Harmony's left or right desktop
sidebars for normal pages. `#panel` should run as the main content column under
the topbar.
### Implementation Principles For Later
This is not implemented in this spec pass, but the later implementation should
follow these principles:
- Add a theme partial such as `templates/partials/header/topbar.tpl`.
- Mount that partial from `templates/header.tpl`.
- Stop importing `partials/sidebar-left.tpl` and `partials/sidebar-right.tpl` in
the global layout, either by overriding both header and footer or by another
clean theme-level layout change.
- Move topbar SCSS into a focused Westgate partial such as
`scss/westgate/_topbar.scss`; import it from `theme.scss`.
- Prefer topbar-specific local partials copied from Harmony sidebar partials only
where the markup must change. Preserve NodeBB component hooks such as
`component="notifications/list"`, `component="chat/list"`,
`component="drafts/list"`, `component="drafts/open"`,
`component="drafts/delete"`, `component="header/usercontrol"`, and
`component="search/form"`.
- Keep Bootstrap dropdown behavior if reusing Harmony's existing client logic.
- If markup cannot preserve Harmony's selectors, update the theme client script
intentionally instead of relying on accidental sidebar selectors.
- Do not duplicate live notification/chat/draft data with static mock rows in
production.
- Ensure the topbar is present on Custom Pages, wiki plugin routes, and standard
forum routes.
### Harmony JavaScript Compatibility Risks
`public/harmony.js` currently assumes sidebar selectors in several places:
- Composer sizing offsets from `.sidebar-left`.
- Sidebar toggle persistence via `[component="sidebar/toggle"]`.
- Search focus via `[component="sidebar/search"]`.
- Draft rendering via `[component="sidebar/drafts"]` and
`[component="drafts/list"]`.
- Notification/chat placeholder cloning via `nav.sidebar`.
- Sidebar tooltip setup via `.sidebar`, `.sidebar-left`, and `.sidebar-right`.
- `#main-nav` overflow adjustment for left sidebar dropdowns.
The implementation must either keep compatible hooks in the topbar or replace
these assumptions with topbar-aware behavior. This is the main technical risk in
the topbar migration.
## Acceptance Criteria
For this design to be ready for implementation planning:
- The repository has a clear source-of-truth distinction between raw Claude
Design exports, reference Custom Pages modules, and tracked `custom/`
production sources.
- The five required custom pages have named production files under `custom/`.
- Static Custom Pages can be installed without relying on Harmony sidebars.
- Dynamic Custom Pages have a documented data ownership path through NodeBB
forum content and/or local plugins.
- The global topbar spec accounts for every current Harmony sidebar user
function, including logged-out and logged-in states.
- News and Blog use forum categories `1` and `84` as canonical content while
presenting as website articles.
- Gallery is explicitly left open where its source model is not yet thoroughly
specced.
- Recruitment is renamed from Join the Team to `recruitment.html` and does not
pretend application functionality exists yet.
- Wiki and forums are covered by the same parity requirements as Custom Pages.
- Routes, asset paths, and live-data integrations are listed as decisions instead
of being silently assumed.
- No generated files are promoted to tracked production assets without an
explicit decision.
## Open Decisions
- Should `custom_pages/` remain untracked reference material, or should selected
QA screenshots be tracked separately?
- Should the global topbar first ship as a theme partial, or temporarily as
Custom Content HTML/JavaScript for faster visual validation?
- Should the skin switcher remain available? If yes, it needs a topbar/user-menu
home.
- Should `sidebar-footer` widget content be deprecated, moved to a real footer,
or exposed somewhere else?
- Should Harmony's mobile bottom/top bar remain enabled after the Westgate
topbar mobile drawer exists?
- What are the final route names and redirects for Home, Recruitment, News,
Gallery, Wiki, Forums, Blog, and Developer Blog?
- What is the canonical Gallery source model?
- What data convention marks a News/Blog topic banner image?
- Which plugin, if any, owns applications when recruitment needs an application
flow?
- Which page screenshots should become QA references, and which are obsolete?
## Non-Goals For This Pass
- No template overrides are implemented here.
- No SCSS is moved or added.
- No Custom Pages content is installed.
- No category IDs are changed.
- No `custom/` production files are created in this investigation-only pass.
- No generated image or screenshot assets are promoted.
- No commits or pushes are made by this draft.