impeccable (#18)
first pass with impeccable Reviewed-on: #18 Reviewed-by: xtul <mpiasecki720@protonmail.com> Co-authored-by: vickydotbat <vickydotbat@tutamail.com> Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
This commit was merged in pull request #18.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
---
|
||||
target: SoW NodeBB theme visual pass
|
||||
total_score: 27
|
||||
p0_count: 1
|
||||
p1_count: 3
|
||||
timestamp: 2026-07-11T12-28-14Z
|
||||
slug: sow-nodebb-theme
|
||||
---
|
||||
# Critique: nodebb-theme-westgate (visual pass)
|
||||
|
||||
Method: dual-agent (A: design-review sub-agent · B: detector-evidence sub-agent), live forum at http://localhost:4567 (NodeBB dev stack, theme rebuilt from working tree @ branch `impeccable`, commit 950f3ba).
|
||||
|
||||
## Design Health Score
|
||||
|
||||
| # | Heuristic | Score | Key Issue |
|
||||
|---|-----------|-------|-----------|
|
||||
| 1 | Visibility of System Status | 3 | Unread counts, watching chips, draft badge all present; icon-only topbar gives weak "where am I" feedback |
|
||||
| 2 | Match System / Real World | 3 | Ledger metaphor fits; "World", "Crosspost Topic", "Manage Open Social Web Handles" are opaque jargon |
|
||||
| 3 | User Control and Freedom | 3 | Discard-with-confirm, Escape closes drawer, breadcrumbs, skip-link |
|
||||
| 4 | Consistency and Standards | 2 | Two primary-button styles; post tables unthemed while notification/flags tables are themed; pink default avatars |
|
||||
| 5 | Error Prevention | 3 | Password rules shown upfront on register; discard confirmation |
|
||||
| 6 | Recognition Rather Than Recall | 2 | 9+ icon-only topbar destinations, tooltip-only labels |
|
||||
| 7 | Flexibility and Efficiency | 3 | Good search filters and sorts; no keyboard-shortcut affordance anywhere |
|
||||
| 8 | Aesthetic and Minimalist Design | 3 | Strong restrained system; noise from zero-count stat boxes and 15 zero-count notification filters |
|
||||
| 9 | Error Recovery | 3 | Themed 404 with home link |
|
||||
| 10 | Help and Documentation | 2 | Wiki landing leaks admin setup copy to guests; no member onboarding |
|
||||
| **Total** | | **27/40** | **Acceptable — solid base, specific fixes needed** |
|
||||
|
||||
## Anti-Patterns Verdict
|
||||
|
||||
**Not AI slop, not a default skin.** The velvet gradients, stepped gold borders, Cinzel/Jost pairing, and custom footer are genuinely art-directed. No gradient text, no glassmorphism, no hero-metric template, no eyebrow scaffolding.
|
||||
|
||||
Two ban-level violations and a set of "default NodeBB" tells:
|
||||
|
||||
- **Side-stripe ban violated (and the theme's own Gilt Edge Rule):** topic rows carry a 2px gold `border-left` accent — `scss/westgate/_topics.scss:64` (with state variants at 76–182) and `_widgets.scss:108–148`. Detector confirmed (`side-tab` ×3 on /topic/2). The rule says gold lives in 1px edges.
|
||||
- **Display font on data:** profile email in Cinzel wraps mid-token ("ADMIN@WESTGATE.LOCA / L"); timestamps as 11.5px uppercase tracked text ("5 MINUTES AGO") — DESIGN.md's Engraving Rule says timestamps keep letter-spacing 0.
|
||||
- **Default-skin tells:** bright magenta auto-generated avatars, Harmony's grey-triangle profile cover, the near-white "Powered by NodeBB / TipTap" footer pills — the loudest pixels on every page are not the theme's.
|
||||
|
||||
**Deterministic scan:** CLI detector cannot read `.tpl` files, so the shipped templates got zero static coverage; 1786 CLI findings all come from `custom_pages/` mockup artifacts (mostly duplicates/dead files — treat as noise, or delete the stale mockups). The real evidence came from injecting the detector into the live pages: recurring cross-page hits were footer meta text at 4.4:1 contrast (6 of 7 pages), gold glow on footer pills and category cards (`dark-glow`), 1px-border + 16–18px-shadow on the brand mark and avatars, tight line-height 1.25 on card teasers, and ~107–155 characters per line in post/search/wiki prose (aim < 80). Detector's extreme claims (1.1:1 text on gold) are sampling artifacts — visually checked, false positives.
|
||||
|
||||
**Overlays:** the detector ran in a headless browser (Playwright MCP could not launch Chrome on NixOS), so there is no user-visible overlay tab; console findings above are the signal.
|
||||
|
||||
## Overall Impression
|
||||
|
||||
The Velvet Ledger system is real and consistently executed — screenshots are unmistakably this forum, and body text (9.2:1) proves atmosphere is not taxing reading. The biggest opportunity is finishing the last 5%: the places where inherited NodeBB/Bootstrap defaults poke through the velvet (invisible post tables, pink avatars, near-white footer pills, dim focus ring). Each one individually is small; together they are the difference between "themed forum" and "in-world artifact."
|
||||
|
||||
## What's Working
|
||||
|
||||
1. **A real material system, applied everywhere.** The 100° velvet gradient + 1px gold borders at stepped alphas + three panel tones appear identically on cards, composer, search, 404, empty states. Hierarchy comes from tone and border strength, not decoration — the Candlelight Rule holds.
|
||||
2. **Accessibility bones are genuinely built in.** Skip-link first in tab order, consistent outline token on all tabbed controls, aria states on the burger, Escape closes the drawer, wiki tables get focusable scroll wrappers with labels. Only the focus token's alpha is wrong (see P1).
|
||||
3. **Thematic parity ~95%.** Login, register, 404, search, composer, notification empty states all stay in-world; zero horizontal scroll on mobile across tested pages.
|
||||
|
||||
## Priority Issues
|
||||
|
||||
1. **[P0] Markdown tables in posts are invisible.** Computed 1.02:1 — near-black text on near-black panel (`rgb(9,8,11)` on `rgb(15,13,18)`). The merchant-price table that is the whole point of the seeded feedback thread cannot be read; any player table (loot splits, event schedules) will hit this. Root cause: `--bs-table-*` overrides exist only for notification tables (`_controls.scss:211`) and flags (`_surfaces.scss:361`); post-content tables fall through to Bootstrap's dark default. Fix: hoist the already-duplicated table-variable block into one shared rule that also covers `[component="post/content"] table`. Both assessments found this independently. → `/impeccable polish`
|
||||
2. **[P1] Focus ring fails non-text contrast.** `--wg-focus: rgba(194,163,90,0.28)` (`_tokens.scss:98`) ≈ 1.7:1 over crypt black (WCAG wants ≥3:1 for focus indicators). Confirmed visually: barely perceptible on the topbar icons. PRODUCT.md demands visible focus; DESIGN.md itself specifies the failing value, so the spec needs the fix too. Fix: solid `#c2a35a` (≈6:1) for outlines; keep the soft token for input glows. → `/impeccable polish`
|
||||
3. **[P1] Mobile drawer shows duplicate navigation.** Categories/Recent/Unread/Tags appear twice: `templates/partials/header/topbar.tpl:99–111` loops `{{{ each navigation }}}` then unconditionally appends four hardcoded links. Reads as a glitch on every mobile visit. Fix: delete or guard the hardcoded block. → `/impeccable polish`
|
||||
4. **[P1] The wiki surface is the weakest page, and it's the front door.** (a) Guests see "Set the Wiki Homepage… You do not have permission" — admin plumbing shown to the visitor most likely arriving from out-of-game links. (b) Console error on every load: `error loading forum/wiki — Cannot find module './wiki'` — the wiki's client-side module is missing from the build (plugin/build issue, worth confirming against production). (c) Wiki card prose runs ~155 chars/line. → `/impeccable harden` (empty/guest states) + a code fix in the wiki plugin or build
|
||||
5. **[P2] Inherited defaults break the palette on every page.** Bright magenta generated avatars, grey-triangle default profile cover, near-white "Powered by NodeBB / TipTap" pills (`templates/partials/footer/westgate-global.tpl:47–50`), footer meta at 4.4:1 (`#7f756f` on `#09080b` — fails AA by a hair). Fix: constrain generated avatar background colors to a plum/gold/dried-blood set, ship a themed default cover, restyle the powered-by pills to ghost-button treatment, bump footer meta one step toward ash. → `/impeccable polish`
|
||||
|
||||
## Persona Red Flags
|
||||
|
||||
**Alex (power user):** 9 icon-only topbar destinations with tooltip-only labels (flame=Popular, globe=World, inbox=Unread) demand memorization; no keyboard-shortcut affordance anywhere; /notifications makes him scan 15 zero-count filter rows. Positive: search dropdown autofocuses its input.
|
||||
|
||||
**Sam (keyboard/screen-reader):** the ~1.7:1 focus ring makes complete keyboard support practically invisible; the 1.02:1 post tables fail WCAG 1.4.3 catastrophically; 11.5px uppercase tracked metadata is the hardest-to-read treatment applied to the most-repeated text. Positives: skip-link, aria states, Escape handling, focusable table scroll wrappers.
|
||||
|
||||
**Returning SoW roleplayer on mobile:** duplicate drawer nav reads as a glitch every visit; the price table they came to check is invisible; "WATCHING" chips stamped on every topic row tax the list they skim most. Positives: no horizontal scroll at 390px; thumb-friendly bottom pager.
|
||||
|
||||
## Minor Observations
|
||||
|
||||
- Core i18n grammar leaks: "1 pages", "2 result(s)", "1 posts".
|
||||
- 404 keeps Harmony's playful otter — mildly off the "nothing playful" anti-reference; an in-world line was free. Same for the notification empty state ("wind" icon).
|
||||
- `public/client.js:140` adds mobile scroll wrappers only to `.westgate-wiki` tables; post-content tables get none.
|
||||
- Card teaser line-height 1.25 (detector `tight-leading` ×3 on home/recent) — bump to ≥1.3.
|
||||
- Two primary-button vocabularies: bright gold gradient (topbar Register, composer Submit) vs the spec'd dark gold plate (Login, "Mark all read"). Gilt Edge Rule says the dark plate is the only gold fill — pick one or amend DESIGN.md.
|
||||
- Post prose ~107 chars/line at desktop width; cap the content column nearer 80ch.
|
||||
- `custom_pages/` carries stale duplicate mockups (`Westgate v1…`, `uploads/Westgate.dc.html`) that dominate any static scan — consider pruning.
|
||||
- Sitewide brand text reads "NODEBB" — dev config value, but verify production siteTitle.
|
||||
|
||||
## Questions to Consider
|
||||
|
||||
1. If gold "marks what can be touched," why is the widest gold on the site a decorative 2px stripe on topic rows — while the most touchable thing, the focus ring, is the dimmest gold in the system?
|
||||
2. The ledger is the court's own book — so why does a guest opening /wiki read the clerk's setup notes, and why are the only near-white surfaces in the theme third-party credits?
|
||||
3. Should user data (emails, timestamps, usernames) ever wear the Cinzel engraving, or only the court's own inscriptions — and what enforces that when the next plugin ships a table?
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"generatedAt": "2026-07-11T00:00:00Z",
|
||||
"title": "Design System: Westgate",
|
||||
"extensions": {
|
||||
"colorMeta": {
|
||||
"tarnished-gold": { "role": "primary", "displayName": "Tarnished Gold", "canonical": "#c2a35a", "tonalRamp": ["#241d0e", "#3d3118", "#574624", "#725c31", "#8f7440", "#c2a35a", "#d9bf7f", "#efdcae"] },
|
||||
"bruised-plum": { "role": "secondary", "displayName": "Bruised Plum", "canonical": "#2a1222", "tonalRamp": ["#160811", "#2a1222", "#3a1830", "#54254a", "#6f3563", "#8b487e", "#b371a5", "#d9a8ce"] },
|
||||
"dried-blood": { "role": "tertiary", "displayName": "Dried-Blood Red", "canonical": "#8e3438", "tonalRamp": ["#20090a", "#3c1416", "#5a2124", "#8e3438", "#a84a4e", "#c26a6d", "#d99093", "#eebcbe"] },
|
||||
"crypt-black": { "role": "neutral", "displayName": "Crypt Black", "canonical": "#0f0d12", "tonalRamp": ["#09080b", "#0f0d12", "#110f15", "#18141d", "#201924", "#3a323c", "#625760", "#9a9086"] },
|
||||
"bone-text": { "role": "neutral", "displayName": "Bone Text", "canonical": "#e6e0d6", "tonalRamp": ["#2b2823", "#4a453d", "#6b645a", "#8d8579", "#9a9086", "#b9b2a6", "#e6e0d6", "#f3ede4"] },
|
||||
"ledger-ink": { "role": "neutral", "displayName": "Ledger Ink", "canonical": "#d8c28a", "tonalRamp": ["#2a2413", "#4a3f22", "#6b5b33", "#8d7846", "#b09659", "#d8c28a", "#e5d4a8", "#f2e7ca"] }
|
||||
},
|
||||
"typographyMeta": {
|
||||
"display": { "displayName": "Display", "purpose": "All headings and titles; engraved Cinzel with a faint etch shadow." },
|
||||
"body": { "displayName": "Body", "purpose": "Posts and long reading; calm geometric Jost." },
|
||||
"label": { "displayName": "Label", "purpose": "Small engraved labels: topbar, footer headings, table headers. The only place tracking and uppercase are allowed." },
|
||||
"code": { "displayName": "Code", "purpose": "Code blocks and inline code." }
|
||||
},
|
||||
"shadows": [
|
||||
{ "name": "velvet-rest", "value": "inset 0 1px 0 rgba(255,255,255,0.025), inset 0 0 34px rgba(96,32,68,0.11), 0 10px 26px rgba(0,0,0,0.24)", "purpose": "Default velvet surface: sheen, plum bloom, ambient depth." },
|
||||
{ "name": "velvet-hover", "value": "inset 0 1px 0 rgba(255,255,255,0.035), inset 0 0 38px rgba(120,36,84,0.14), 0 12px 30px rgba(0,0,0,0.3)", "purpose": "Hover state of velvet surfaces; warmer bloom, slightly deeper." },
|
||||
{ "name": "ledger-rest", "value": "inset 0 1px 0 rgba(255,255,255,0.024), inset 0 -1px 0 rgba(0,0,0,0.42), 0 8px 18px rgba(0,0,0,0.2)", "purpose": "Flatter page-like ledger surfaces." },
|
||||
{ "name": "control-depth", "value": "inset 0 1px 0 rgba(255,244,221,0.04), 0 2px 8px rgba(0,0,0,0.22)", "purpose": "Small ambient depth on buttons and controls." }
|
||||
],
|
||||
"motion": [],
|
||||
"breakpoints": []
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"name": "Primary Button",
|
||||
"kind": "button",
|
||||
"refersTo": "button-primary",
|
||||
"description": "Dark gold plate with gilt border; the main call to action.",
|
||||
"html": "<button class=\"ds-btn-primary\">Post Reply</button>",
|
||||
"css": ".ds-btn-primary { font-family: Jost, system-ui, sans-serif; font-size: 0.95rem; color: #f0e6d8; background: linear-gradient(to bottom, #5a4a1a 0%, #473914 100%); border: 1px solid #a8893f; border-radius: 6px; padding: 8px 20px; cursor: pointer; box-shadow: inset 0 1px 0 rgba(255,255,255,0.06), 0 2px 8px rgba(0,0,0,0.24); transition: background 0.15s, border-color 0.15s; } .ds-btn-primary:hover { color: #fff4dd; background: linear-gradient(to bottom, #6a5720 0%, #53431a 100%); border-color: #c2a35a; } .ds-btn-primary:focus-visible { outline: 2px solid rgba(194,163,90,0.28); outline-offset: 2px; }"
|
||||
},
|
||||
{
|
||||
"name": "Ghost Button",
|
||||
"kind": "button",
|
||||
"refersTo": "button-ghost",
|
||||
"description": "Near-transparent dark button with a gilt edge; secondary actions.",
|
||||
"html": "<button class=\"ds-btn-ghost\">Cancel</button>",
|
||||
"css": ".ds-btn-ghost { font-family: Jost, system-ui, sans-serif; font-size: 0.95rem; color: #d8cec0; background: linear-gradient(to bottom, rgba(255,244,221,0.018), rgba(0,0,0,0.06)), rgba(8,7,10,0.12); border: 1px solid rgba(216,194,138,0.28); border-radius: 6px; padding: 8px 20px; cursor: pointer; box-shadow: inset 0 1px 0 rgba(255,244,221,0.035); transition: background 0.15s, border-color 0.15s; } .ds-btn-ghost:hover { color: #e6e0d6; background: rgba(194,163,90,0.075); border-color: rgba(216,194,138,0.44); } .ds-btn-ghost:focus-visible { outline: 2px solid rgba(194,163,90,0.28); outline-offset: 2px; }"
|
||||
},
|
||||
{
|
||||
"name": "Text Input",
|
||||
"kind": "input",
|
||||
"refersTo": "input",
|
||||
"description": "Near-black well with gilt border; gold ring on focus.",
|
||||
"html": "<input class=\"ds-input\" type=\"text\" placeholder=\"Search Westgate...\" />",
|
||||
"css": ".ds-input { font-family: Jost, system-ui, sans-serif; font-size: 1rem; color: #e6e0d6; background-color: rgba(18,15,22,0.96); border: 1px solid rgba(194,163,90,0.18); border-radius: 6px; padding: 8px 12px; width: 260px; transition: border-color 0.15s, box-shadow 0.15s; } .ds-input::placeholder { color: rgba(185,178,166,0.62); } .ds-input:focus { outline: none; background-color: rgba(255,255,255,0.04); border-color: rgba(194,163,90,0.5); box-shadow: inset 0 1px 0 rgba(255,255,255,0.026), 0 0 0 1px rgba(194,163,90,0.28); }"
|
||||
},
|
||||
{
|
||||
"name": "Velvet Card",
|
||||
"kind": "card",
|
||||
"refersTo": "card",
|
||||
"description": "The signature velvet surface used for category rows, topics, and posts.",
|
||||
"html": "<div class=\"ds-velvet-card\"><h3 class=\"ds-velvet-title\">The Harbor District</h3><p class=\"ds-velvet-body\">Rumors travel faster than ships. Post what you have heard on the docks.</p></div>",
|
||||
"css": ".ds-velvet-card { 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%), #0f0d12; border: 1px solid rgba(194,163,90,0.14); border-radius: 8px; padding: 16px 20px; max-width: 420px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.025), inset 0 0 34px rgba(96,32,68,0.11), 0 10px 26px rgba(0,0,0,0.24); transition: border-color 0.2s, box-shadow 0.2s; } .ds-velvet-card:hover { border-color: rgba(194,163,90,0.22); box-shadow: inset 0 1px 0 rgba(255,255,255,0.035), inset 0 0 38px rgba(120,36,84,0.14), 0 12px 30px rgba(0,0,0,0.3); } .ds-velvet-title { font-family: Cinzel, Georgia, serif; font-weight: 600; font-size: 1.1rem; color: #e6e0d6; margin: 0 0 6px; text-shadow: 0 1px 0 rgba(0,0,0,0.9), 0 0 10px rgba(104,32,76,0.16); } .ds-velvet-body { font-family: Jost, system-ui, sans-serif; font-size: 0.95rem; color: #b9b2a6; margin: 0; }"
|
||||
},
|
||||
{
|
||||
"name": "Ledger Panel",
|
||||
"kind": "custom",
|
||||
"refersTo": "card",
|
||||
"description": "Ruled page-like surface for tables and lists, written in ledger ink.",
|
||||
"html": "<div class=\"ds-ledger\"><div class=\"ds-ledger-head\">Notifications</div><div class=\"ds-ledger-row\">A letter arrives from the Night Court</div><div class=\"ds-ledger-row\">Your application was <span class=\"ds-ledger-stamp\">stamped</span></div></div>",
|
||||
"css": ".ds-ledger { background: linear-gradient(100deg, rgba(38,22,35,0.5) 0%, rgba(19,17,24,0.97) 34%, rgba(13,12,17,0.98) 100%), #0f0d12; border: 1px solid rgba(194,163,90,0.18); border-radius: 8px; max-width: 420px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.024), inset 0 -1px 0 rgba(0,0,0,0.42), 0 8px 18px rgba(0,0,0,0.2); } .ds-ledger-head { font-family: Cinzel, Georgia, serif; font-weight: 600; font-size: 0.86rem; letter-spacing: 0.02em; color: #a99d8f; padding: 10px 16px; border-bottom: 1px solid rgba(194,163,90,0.16); } .ds-ledger-row { font-family: Jost, system-ui, sans-serif; font-size: 0.95rem; color: #d8c28a; padding: 10px 16px; border-bottom: 1px solid rgba(194,163,90,0.055); } .ds-ledger-row:last-child { border-bottom: none; } .ds-ledger-stamp { color: #a84a4e; font-weight: 600; }"
|
||||
}
|
||||
],
|
||||
"narrative": {
|
||||
"northStar": "The Velvet Ledger",
|
||||
"overview": "The forum is the account book of a vampire court: pages of near-black velvet, ruled with faint gold lines, written in tarnished gilt ink, stamped here and there with a dried-blood seal. Every surface is dark and plush. Gold appears as thin edges, rulings, and small marks of importance — the way gilt survives on an old book long after the leather has gone dark. Nothing is bright. Nothing is flat. Depth comes from fabric and candlelight, not from floating cards. It is a working forum first: text stays readable, focus stays visible, and the atmosphere never taxes reading.",
|
||||
"keyCharacteristics": [
|
||||
"Near-black plum-tinted surfaces with velvet gradient sheen",
|
||||
"Tarnished gold carries all borders, links, and affordances",
|
||||
"Engraved serif headings (Cinzel) over calm geometric body text (Jost)",
|
||||
"Red reserved for states and small stamped details",
|
||||
"Ambient, candlelit shadows; hierarchy by panel tone, not lift"
|
||||
],
|
||||
"rules": [
|
||||
{ "name": "The Gilt Edge Rule", "body": "Gold is edging, not paint. It lives in 1px borders, thin rules, icons, and text. The only gold-filled surface in the theme is the primary button, and even that is a dark #5a4a1a, not bright gold.", "section": "colors" },
|
||||
{ "name": "The Sparing Red Rule", "body": "Red is a state and a detail — an error, a seal, a warning. It is never a background, never decoration, never loud.", "section": "colors" },
|
||||
{ "name": "The Engraving Rule", "body": "Wide letter-spacing and uppercase belong only to small Cinzel labels. Headings at reading size keep letter-spacing 0. Body text is never tracked, never uppercase.", "section": "typography" },
|
||||
{ "name": "The Candlelight Rule", "body": "Shadows are atmosphere. If you need to show one thing sits above another, change the panel tone and border alpha; do not invent a bigger shadow.", "section": "elevation" }
|
||||
],
|
||||
"dos": [
|
||||
"Do keep body text at or above 4.5:1 contrast; Ash Text (#b9b2a6) on Crypt Black is the floor. Drop to Dust Text only for genuinely secondary metadata.",
|
||||
"Do carry the theme to every surface — wiki, custom pages, applications, reports, admin-facing lists. One unthemed page breaks the spell.",
|
||||
"Do build hierarchy with the three panel tones and border alpha steps (8% / 14% / 18–22%), per the Candlelight Rule.",
|
||||
"Do keep a visible 2px gold focus outline on everything interactive.",
|
||||
"Do stay a small child theme: inherit from Harmony and override only what Westgate needs."
|
||||
],
|
||||
"donts": [
|
||||
"Don't make anything \"bright, playful, clinical, or flat SaaS-style\" (PRODUCT.md). No white surfaces, no saturated blues, no flat borderless cards.",
|
||||
"Don't add \"loud ornament\" (PRODUCT.md). No filigree backgrounds, no large gold fills, no glow effects beyond the existing plum bloom.",
|
||||
"Don't use red as a surface or decoration — states and small seals only (the Sparing Red Rule).",
|
||||
"Don't track or uppercase body text; that treatment belongs to small Cinzel labels only (the Engraving Rule).",
|
||||
"Don't ship anything that could pass as a generic default forum skin. If a screenshot could be any NodeBB forum, it is not done."
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,49 @@ compiled in.
|
||||
|
||||
## How It Is Used
|
||||
|
||||
There is no build step in this repo; NodeBB compiles the SCSS. Local check:
|
||||
`node scripts/check-emails.js` (if working on email templates). To see changes
|
||||
live, use the `docker-compose.dev.yml` dev loop in sow-nodebb with this repo
|
||||
as an extra mounted package. Release: merge here, re-pin the commit SHA in
|
||||
sow-nodebb's `plugins.lock`, rebuild and publish the image there.
|
||||
There is no build step in this repo; NodeBB compiles the SCSS. To see changes
|
||||
live, use the `docker-compose.dev.yml` dev loop in sow-nodebb (see How To Test
|
||||
below). Release: merge here, re-pin the commit SHA in sow-nodebb's
|
||||
`plugins.lock`, rebuild and publish the image there.
|
||||
|
||||
## How To Test
|
||||
|
||||
Fast checks, no forum needed:
|
||||
|
||||
- `node tests/<name>.test.js` — contract tests (topbar, footer, icons, wiki
|
||||
tables). There is no `npm test` script; run each file directly.
|
||||
- `node scripts/check-emails.js` — when working on email templates.
|
||||
|
||||
Live visual check against a real forum, using the sibling checkout at
|
||||
`../sow-nodebb`:
|
||||
|
||||
1. `cd ../sow-nodebb && cp .env.example .env` (if not done already), then set
|
||||
`PLUGIN_PATH=../sow-nodebb-theme` and `PLUGIN_ID=nodebb-theme-westgate` so
|
||||
the compose file mounts this working tree.
|
||||
2. `docker compose -f docker-compose.dev.yml up`
|
||||
3. Open `http://localhost:4567` (or `HTTP_PORT` from `.env`). Dev admin login:
|
||||
`admin` / `Admin12345!` (defaults; dev-only).
|
||||
4. If Westgate is not the active theme yet, switch to it once in
|
||||
ACP → Appearance → Themes; the choice persists in the redis volume.
|
||||
5. NodeBB compiles this theme's SCSS and templates at build time, so a browser
|
||||
reload alone is not enough after editing them. Rebuild, then reload:
|
||||
`docker compose -f docker-compose.dev.yml exec nodebb ./nodebb build --config=/opt/config/config.json`
|
||||
(restart the `nodebb` service if changes still don't show).
|
||||
6. Full reset (also required after changing `PLUGIN_*` in `.env`):
|
||||
`docker compose -f docker-compose.dev.yml down -v`
|
||||
|
||||
Agents with browser tooling (e.g. Playwright) should point it at
|
||||
`http://localhost:4567` to load pages, take screenshots, and verify theme
|
||||
changes on the live forum.
|
||||
|
||||
## Design Context
|
||||
|
||||
- `PRODUCT.md`: strategic design context — register, users, purpose,
|
||||
positioning, brand personality, anti-references, design principles,
|
||||
accessibility target (WCAG AA). Read it before design work.
|
||||
- `DESIGN.md`: the visual system — palette ("The Velvet Ledger"), typography,
|
||||
elevation, components, and do's/don'ts, with machine-readable tokens in its
|
||||
frontmatter and `.impeccable/design.json`.
|
||||
|
||||
## Guidance Map
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
---
|
||||
name: Westgate
|
||||
description: Dark velvet-and-gold NodeBB theme for Shadows Over Westgate
|
||||
colors:
|
||||
tarnished-gold: "#c2a35a"
|
||||
old-gilt: "#a8893f"
|
||||
gold-lit: "#e0c878"
|
||||
bruised-plum: "#2a1222"
|
||||
plum-bloom: "#3a1830"
|
||||
dried-blood: "#8e3438"
|
||||
wax-seal: "#a84a4e"
|
||||
crypt-black: "#0f0d12"
|
||||
grave-black: "#09080b"
|
||||
velvet-panel: "#18141d"
|
||||
velvet-panel-deep: "#110f15"
|
||||
velvet-panel-raised: "#201924"
|
||||
bone-text: "#e6e0d6"
|
||||
ash-text: "#b9b2a6"
|
||||
dust-text: "#9a9086"
|
||||
ledger-ink: "#d8c28a"
|
||||
ledger-ink-muted: "#a99d8f"
|
||||
typography:
|
||||
display:
|
||||
fontFamily: "Cinzel, Georgia, 'Times New Roman', serif"
|
||||
fontWeight: 600
|
||||
letterSpacing: "0"
|
||||
body:
|
||||
fontFamily: "Jost, system-ui, sans-serif"
|
||||
fontSize: "1rem"
|
||||
fontWeight: 400
|
||||
label:
|
||||
fontFamily: "Cinzel, Georgia, serif"
|
||||
fontSize: "0.86rem"
|
||||
fontWeight: 600
|
||||
letterSpacing: "0.08em"
|
||||
code:
|
||||
fontFamily: "'IBM Plex Mono', ui-monospace, monospace"
|
||||
fontWeight: 400
|
||||
rounded:
|
||||
md: "8px"
|
||||
spacing:
|
||||
2xs: "0.25rem"
|
||||
xs: "0.5rem"
|
||||
sm: "0.75rem"
|
||||
md: "1rem"
|
||||
lg: "1.5rem"
|
||||
xl: "2rem"
|
||||
components:
|
||||
button-primary:
|
||||
backgroundColor: "#5a4a1a"
|
||||
textColor: "#f0e6d8"
|
||||
button-primary-hover:
|
||||
backgroundColor: "#6a5720"
|
||||
textColor: "#fff4dd"
|
||||
button-ghost:
|
||||
backgroundColor: "#08070a1f"
|
||||
textColor: "#d8cec0"
|
||||
input:
|
||||
backgroundColor: "#120f16f5"
|
||||
textColor: "{colors.bone-text}"
|
||||
card:
|
||||
backgroundColor: "{colors.velvet-panel}"
|
||||
textColor: "{colors.ash-text}"
|
||||
rounded: "{rounded.md}"
|
||||
---
|
||||
|
||||
# Design System: Westgate
|
||||
|
||||
## 1. Overview
|
||||
|
||||
**Creative North Star: "The Velvet Ledger"**
|
||||
|
||||
The forum is the account book of a vampire court: pages of near-black velvet, ruled with faint gold lines, written in tarnished gilt ink, stamped here and there with a dried-blood seal. Every surface is dark and plush. Gold appears as thin edges, rulings, and small marks of importance — the way gilt survives on an old book long after the leather has gone dark. Nothing is bright. Nothing is flat. Depth comes from fabric and candlelight, not from floating cards.
|
||||
|
||||
This system explicitly rejects anything bright, playful, clinical, or flat SaaS-style, and any loud ornament. It must never read as a generic default forum skin. It is a working forum first: text stays readable, focus stays visible, and the atmosphere never taxes reading.
|
||||
|
||||
**Key Characteristics:**
|
||||
- Near-black plum-tinted surfaces with velvet gradient sheen
|
||||
- Tarnished gold carries all borders, links, and affordances
|
||||
- Engraved serif headings (Cinzel) over calm geometric body text (Jost)
|
||||
- Red reserved for states and small stamped details
|
||||
- Ambient, candlelit shadows; hierarchy by panel tone, not lift
|
||||
|
||||
## 2. Colors
|
||||
|
||||
A tarnish-and-decay palette: gold that has aged, plum that has bruised, black that has settled.
|
||||
|
||||
### Primary
|
||||
- **Tarnished Gold** (#c2a35a): the voice of the theme. Links, borders (usually at 8–18% alpha), icons, focus rings, selection. It marks what can be touched.
|
||||
- **Old Gilt** (#a8893f): darker gold for pressed states and primary-button borders.
|
||||
- **Gold Lit** (#e0c878): link hover — the gilt catching candlelight.
|
||||
|
||||
### Secondary
|
||||
- **Bruised Plum** (#2a1222): the tint inside velvet gradients and inner glows. Almost never a flat fill; it blooms through surfaces.
|
||||
- **Plum Bloom** (#3a1830): the brighter bruise, used at hover in the velvet gradients.
|
||||
|
||||
### Tertiary
|
||||
- **Dried-Blood Red** (#8e3438): danger and warning states, small details. Never a large surface.
|
||||
- **Wax Seal** (#a84a4e): stamped accents inside ledger surfaces.
|
||||
|
||||
### Neutral
|
||||
- **Crypt Black** (#0f0d12): the body background.
|
||||
- **Grave Black** (#09080b): the deepest layer (status ring backdrops, page depths).
|
||||
- **Velvet Panel** (#18141d), **Velvet Panel Deep** (#110f15), **Velvet Panel Raised** (#201924): the three panel tones that build hierarchy.
|
||||
- **Bone Text** (#e6e0d6): headings and emphasized text.
|
||||
- **Ash Text** (#b9b2a6): body text.
|
||||
- **Dust Text** (#9a9086): muted and secondary text.
|
||||
- **Ledger Ink** (#d8c28a) / **Ledger Ink Muted** (#a99d8f): text written on ledger surfaces.
|
||||
|
||||
### Named Rules
|
||||
**The Gilt Edge Rule.** Gold is edging, not paint. It lives in 1px borders, thin rules, icons, and text. The only gold-filled surface in the theme is the primary button, and even that is a dark #5a4a1a, not bright gold.
|
||||
|
||||
**The Sparing Red Rule.** Red is a state and a detail — an error, a seal, a warning. It is never a background, never decoration, never loud.
|
||||
|
||||
## 3. Typography
|
||||
|
||||
**Display Font:** Cinzel (with Georgia, 'Times New Roman', serif)
|
||||
**Body Font:** Jost (with system-ui, sans-serif)
|
||||
**Code Font:** IBM Plex Mono (with ui-monospace, monospace)
|
||||
|
||||
**Character:** An engraved Roman capital face over a calm geometric sans. Cinzel gives headings the feel of letters cut into stone or stamped in gilt; Jost keeps long forum threads quiet and easy to read. The pairing contrasts on a real axis (carved serif vs geometric sans), so each has a clear job.
|
||||
|
||||
### Hierarchy
|
||||
- **Display / Headings** (Cinzel 600, letter-spacing 0): all h1–h5, category titles, topic titles. Carries a faint etch: `text-shadow: 0 1px 0 rgba(0,0,0,0.9), 0 0 10px rgba(104,32,76,0.16)`.
|
||||
- **Label** (Cinzel 600, ~0.86rem, tracked 0.08–0.16em, often uppercase): topbar items, footer headings, table headers, search labels. The wider tracking is reserved for these small engraved labels.
|
||||
- **Body** (Jost 400, 1rem): posts, descriptions, lists. UI metadata (timestamps, stats) also uses Jost with letter-spacing 0.
|
||||
- **Code** (IBM Plex Mono 400/500): code blocks and inline code.
|
||||
|
||||
### Named Rules
|
||||
**The Engraving Rule.** Wide letter-spacing and uppercase belong only to small Cinzel labels. Headings at reading size keep letter-spacing 0. Body text is never tracked, never uppercase.
|
||||
|
||||
## 3½. Spacing & Layout
|
||||
|
||||
Tokens in `_tokens.scss`: `--wg-space-2xs/xs/sm/md/lg/xl` (0.25 / 0.5 / 0.75 / 1 / 1.5 / 2rem, a 4pt scale). Surface-level rules:
|
||||
|
||||
- Card interiors pad `--wg-space-md`; compact control bars (topic-list header) pad `--wg-space-xs`.
|
||||
- Gaps between sibling cards/widgets are `--wg-space-sm`; below `lg` card padding also drops to `--wg-space-sm`.
|
||||
- The ledger ruling row height is one token everywhere: `--wg-ledger-line` (1.78rem).
|
||||
- Page rails (topbar, footer, content) share `--wg-page-width` (1420px).
|
||||
- Fine optical values (tag chips, badge padding, etched insets) stay hand-tuned; the scale governs surface-level spacing, not micro-typography.
|
||||
|
||||
## 4. Elevation
|
||||
|
||||
Shadows here are candlelight, not physics. They set mood — a soft top highlight like sheen on fabric, an inner plum glow, a wide black falloff — and they do not communicate stacking. Hierarchy between surfaces is carried by panel tone (Velvet Panel Deep → Velvet Panel → Velvet Panel Raised) and by border strength (gold at 8% → 14% → 18–22% alpha).
|
||||
|
||||
### Shadow Vocabulary
|
||||
- **Velvet rest** (`inset 0 1px 0 rgba(255,255,255,0.025), inset 0 0 34px rgba(96,32,68,0.11), 0 10px 26px rgba(0,0,0,0.24)`): the default surface shadow — sheen, bruise, and depth.
|
||||
- **Velvet hover** (`inset 0 1px 0 rgba(255,255,255,0.035), inset 0 0 38px rgba(120,36,84,0.14), 0 12px 30px rgba(0,0,0,0.3)`): the same material, slightly warmed and lifted on hover.
|
||||
- **Ledger rest** (`inset 0 1px 0 rgba(255,255,255,0.024), inset 0 -1px 0 rgba(0,0,0,0.42), 0 8px 18px rgba(0,0,0,0.2)`): flatter, page-like; for ledger surfaces.
|
||||
- **Control depth** (`inset 0 1px 0 rgba(255,244,221,0.04), 0 2px 8px rgba(0,0,0,0.22)`): small ambient depth on buttons.
|
||||
|
||||
### Named Rules
|
||||
**The Candlelight Rule.** Shadows are atmosphere. If you need to show one thing sits above another, change the panel tone and border alpha; do not invent a bigger shadow.
|
||||
|
||||
## 5. Components
|
||||
|
||||
Etched and restrained: controls read as engraved metal — thin gilt borders, quiet vertical gradients, nothing shouts.
|
||||
|
||||
### Buttons
|
||||
- **Shape:** softly rounded (Bootstrap default radius; surfaces use 8px).
|
||||
- **Primary:** dark gold plate. Gradient #5a4a1a → #473914, border Old Gilt (#a8893f), text #f0e6d8, with a 1px inner top highlight. Hover warms the gradient to #6a5720 → #53431a and brightens the border to Tarnished Gold.
|
||||
- **Secondary / Ghost:** near-transparent dark fill (rgba(8,7,10,0.12)) with a gilt border at 28% alpha and text #d8cec0. Hover adds a faint gold wash (rgba(194,163,90,0.075)) and strengthens the border.
|
||||
- **Focus:** 2px outline in solid Tarnished Gold (#c2a35a, the `--wg-focus-ring` token) — WCAG wants ≥3:1 for focus indicators, so the outline never uses the soft alpha token. The soft token (rgba(194,163,90,0.28)) is only for input glows.
|
||||
|
||||
### Cards / Containers (the velvet surface)
|
||||
- **Corner Style:** 8px radius.
|
||||
- **Background:** the velvet gradient — a 100° sweep from bruised plum (rgba(42,18,34,0.46)) into near-black — over category rows, topic rows, post containers, and headers.
|
||||
- **Border:** 1px Tarnished Gold at 14% alpha; hover raises it to 22%.
|
||||
- **Shadow Strategy:** Velvet rest → Velvet hover (see Elevation).
|
||||
- **Plain panels** (cards, list-group items) use flat Velvet Panel (#18141d) with a soft gold border at 8% alpha.
|
||||
|
||||
### Inputs / Fields
|
||||
- **Style:** near-black well (rgba(18,15,22,0.96)), 1px gilt border at 18% alpha, Bone Text. Placeholders are Ash Text at 62% alpha.
|
||||
- **Focus:** border rises to gold at 50% alpha plus a 1px gold ring (rgba(194,163,90,0.28)) and a faint light wash. Focus is always visible.
|
||||
- **Selects:** custom gold chevron (inline SVG, stroke #c2a35a).
|
||||
- **Switches:** dark track with gilt border; the thumb is a Ledger Ink dot.
|
||||
|
||||
### Navigation (topbar)
|
||||
- Cinzel labels, tracked 0.08em and uppercase, over the dark base. Count bubbles honor the `hidden` class and only show with a real count.
|
||||
|
||||
### The Ledger (signature component)
|
||||
Ruled, page-like surfaces (notification tables, list ledgers): a flat 100° gradient from dark plum into black, faint gold rulings (rgba(194,163,90,0.055)) with stronger rules (0.16) for structure, headers in Cinzel Ledger Ink Muted, text in Ledger Ink, and Wax Seal (#a84a4e) for stamped accents.
|
||||
|
||||
## 6. Do's and Don'ts
|
||||
|
||||
### Do:
|
||||
- **Do** keep body text at or above 4.5:1 contrast; Ash Text (#b9b2a6) on Crypt Black is the floor. Drop to Dust Text only for genuinely secondary metadata.
|
||||
- **Do** carry the theme to every surface — wiki, custom pages, applications, reports, admin-facing lists. One unthemed page breaks the spell.
|
||||
- **Do** build hierarchy with the three panel tones and border alpha steps (8% / 14% / 18–22%), per the Candlelight Rule.
|
||||
- **Do** keep a visible 2px gold focus outline on everything interactive.
|
||||
- **Do** stay a small child theme: inherit from Harmony and override only what Westgate needs.
|
||||
|
||||
### Don't:
|
||||
- **Don't** make anything "bright, playful, clinical, or flat SaaS-style" (PRODUCT.md). No white surfaces, no saturated blues, no flat borderless cards.
|
||||
- **Don't** add "loud ornament" (PRODUCT.md). No filigree backgrounds, no large gold fills, no glow effects beyond the existing plum bloom.
|
||||
- **Don't** use red as a surface or decoration — states and small seals only (the Sparing Red Rule).
|
||||
- **Don't** track or uppercase body text; that treatment belongs to small Cinzel labels only (the Engraving Rule).
|
||||
- **Don't** ship anything that could pass as a generic default forum skin. If a screenshot could be any NodeBB forum, it is not done.
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# Product
|
||||
|
||||
## Register
|
||||
|
||||
product
|
||||
|
||||
## Platform
|
||||
|
||||
web
|
||||
|
||||
## Users
|
||||
|
||||
Active Shadows Over Westgate players. They already play on the Neverwinter Nights persistent-world server and use the forum day to day: reading announcements, posting in-character and out-of-character threads, and browsing the wiki. The forum is the whole SoW website, so the same theme also greets anyone who lands on any SoW page.
|
||||
|
||||
## Product Purpose
|
||||
|
||||
This is `nodebb-theme-westgate`, the visual layer for the SoW community forum and website (NodeBB, child theme of Harmony). NodeBB is used for everything: plugins add custom pages and user-interaction flows such as applications and reports, and this theme must cover those surfaces too. It exists so the community's web home looks and feels like the game world it belongs to. Success is immersion plus usability: the forum feels like part of Westgate while staying fast and easy to read.
|
||||
|
||||
## Positioning
|
||||
|
||||
SoW's visual home. The forum serves the entire website, so every page — boards, wiki, profiles, search, custom pages — carries the same theme with full thematic parity. No screen falls back to a plain, out-of-world look.
|
||||
|
||||
## Brand Personality
|
||||
|
||||
Black velvet silk, darkness, vampirism, decadence, and decay. Plum and near-black as the dominant base, muted gold for accents and important affordances, red only sparingly as a subtle detail or state color. Depth, restraint, texture, and rich contrast over loud ornament.
|
||||
|
||||
## Anti-references
|
||||
|
||||
Not bright, playful, clinical, or flat SaaS-style. No loud ornament. Nothing that reads as a generic default forum skin.
|
||||
|
||||
## Design Principles
|
||||
|
||||
- Thematic parity everywhere: every surface, including edge pages and admin-facing lists, stays in-world. An unthemed page breaks the spell.
|
||||
- Atmosphere never taxes reading: the dark palette and texture must not slow down scanning threads or posting. Usability failures are theme bugs.
|
||||
- Restraint over ornament: depth comes from texture, contrast, and material (velvet panels, etched gold), not from decoration piled on.
|
||||
- Stay a small child theme: override only what Westgate needs; inherit the rest from Harmony rather than rebuilding it.
|
||||
|
||||
## Accessibility & Inclusion
|
||||
|
||||
WCAG AA: at least 4.5:1 contrast for body text on the dark palette, visible focus states, usable hover and active states, and reduced-motion alternatives for animation. No text hidden purely for visual effect without an accessible alternative.
|
||||
@@ -1,563 +0,0 @@
|
||||
<!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="{"$preview":{"width":1280,"height":860},"startPage":{"editor":"enum","options":["home","news","blog","gallery","recruit"],"default":"home","tsType":"'home'|'news'|'blog'|'gallery'|'recruit'"}}">
|
||||
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>
|
||||
@@ -1,563 +0,0 @@
|
||||
<!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="{"$preview":{"width":1280,"height":860},"startPage":{"editor":"enum","options":["home","news","blog","gallery","recruit"],"default":"home","tsType":"'home'|'news'|'blog'|'gallery'|'recruit'"}}">
|
||||
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>
|
||||
@@ -1,643 +0,0 @@
|
||||
// @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.
|
Before Width: | Height: | Size: 329 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 134 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 940 KiB |
File diff suppressed because it is too large
Load Diff
@@ -175,7 +175,7 @@
|
||||
</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><div class="big">14 <small>souls online</small></div><div class="sub">PEAK TODAY: 32</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>
|
||||
@@ -217,7 +217,7 @@
|
||||
<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>
|
||||
<a class="wg-more" href="/news">All News →</a>
|
||||
</div>
|
||||
<div class="wg-news">
|
||||
<article class="wg-card">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"reset_notify.preheader": "Your Shadows Over Westgate password was changed",
|
||||
"registration_accepted.preheader": "Your Shadows Over Westgate account is approved",
|
||||
"invitation.preheader": "You have been invited to Shadows Over Westgate",
|
||||
"banned.preheader": "Your Shadows Over Westgate account status has changed",
|
||||
"banned.preheader": "Your Shadows Over Westgate account has been banned",
|
||||
"test.preheader": "Test email from Shadows Over Westgate",
|
||||
"notification.preheader": "You have a new notification on Shadows Over Westgate",
|
||||
"digest.preheader": "Your Shadows Over Westgate activity digest"
|
||||
|
||||
@@ -177,3 +177,17 @@ library.filterMiddlewareRenderHeader = async function (hookData) {
|
||||
hookData.templateData.bootswatchSkinOptions = await meta.css.getSkinSwitcherOptions(hookData.req.uid);
|
||||
return hookData;
|
||||
};
|
||||
|
||||
// generated avatars stay in the court's palette instead of core's material-design brights;
|
||||
// core deterministically remaps any stored color not in this list (src/user/data.js)
|
||||
library.getIconBackgrounds = async function (data) {
|
||||
data.iconBackgrounds = [
|
||||
'#2a1222', // bruised plum
|
||||
'#3a1830', // plum bloom
|
||||
'#4a2433', // plum-red blend
|
||||
'#8e3438', // dried blood
|
||||
'#5a4a1a', // dark gold plate
|
||||
'#201924', // velvet panel raised
|
||||
];
|
||||
return data;
|
||||
};
|
||||
|
||||
+2
-1
@@ -8,7 +8,8 @@
|
||||
{ "hook": "filter:settings.get", "method": "getAdminSettings" },
|
||||
{ "hook": "filter:user.saveSettings", "method": "saveUserSettings" },
|
||||
{ "hook": "filter:user.profileMenu", "method": "addProfileItem" },
|
||||
{ "hook": "filter:middleware.renderHeader", "method": "filterMiddlewareRenderHeader" }
|
||||
{ "hook": "filter:middleware.renderHeader", "method": "filterMiddlewareRenderHeader" },
|
||||
{ "hook": "filter:user.iconBackgrounds", "method": "getIconBackgrounds" }
|
||||
],
|
||||
"scripts": ["public/client.js", "../nodebb-theme-harmony/public/harmony.js"],
|
||||
"templates": "templates",
|
||||
|
||||
+4
-1
@@ -16,7 +16,10 @@
|
||||
|
||||
(function () {
|
||||
const WG_TABLE_SCROLL_CLASS = 'wg-mobile-table-scroll';
|
||||
const WIKI_TABLE_SELECTOR = '.westgate-wiki .wiki-page-content.wiki-article-prose > .card-body table';
|
||||
const WIKI_TABLE_SELECTOR = [
|
||||
'.westgate-wiki .wiki-page-content.wiki-article-prose > .card-body table',
|
||||
'li[component="post"] .content table',
|
||||
].join(', ');
|
||||
const WIKI_TABLE_MIN_COLUMN_WIDTH = 40;
|
||||
const WIKI_TABLE_DESKTOP_MIN_WIDTH = 720;
|
||||
const SKIP_TABLE_SELECTOR = [
|
||||
|
||||
@@ -7,7 +7,7 @@ ul.categories-list,
|
||||
ul.westgate-categories-list {
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
gap: var(--wg-space-sm);
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
@@ -15,13 +15,12 @@ li[component="categories/category"] {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
margin: 0 !important;
|
||||
padding: 1rem !important;
|
||||
padding: var(--wg-space-md) !important;
|
||||
overflow: visible;
|
||||
transition:
|
||||
background 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
li[component="categories/category"].unread {
|
||||
@@ -29,8 +28,15 @@ li[component="categories/category"].unread {
|
||||
}
|
||||
|
||||
li[component="categories/category"] .icon {
|
||||
border: 1px solid rgba(246, 223, 163, 0.34);
|
||||
border: 1px solid var(--wg-etched-highlight);
|
||||
border-radius: 6px !important;
|
||||
// ACP colors arrive candy-bright; the plum wash and filter pull the
|
||||
// plate into the velvet register while keeping the per-category hue
|
||||
background-image: linear-gradient(
|
||||
160deg,
|
||||
rgba(42, 18, 34, 0.42),
|
||||
rgba(9, 8, 11, 0.58)
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 242, 204, 0.28),
|
||||
inset 0 0 0 1px rgba(18, 15, 22, 0.62),
|
||||
@@ -38,7 +44,7 @@ li[component="categories/category"] .icon {
|
||||
0 0 0 1px rgba(56, 48, 42, 0.9),
|
||||
0 0 0 2px rgba(124, 105, 62, 0.24),
|
||||
0 9px 18px rgba(0, 0, 0, 0.32);
|
||||
filter: saturate(0.82) contrast(1.12);
|
||||
filter: saturate(0.62) contrast(1.12);
|
||||
text-shadow:
|
||||
0 1px 0 rgba(0, 0, 0, 0.72),
|
||||
0 -1px 0 rgba(255, 238, 190, 0.18),
|
||||
@@ -46,7 +52,13 @@ li[component="categories/category"] .icon {
|
||||
}
|
||||
|
||||
body.template-category .category-header .icon {
|
||||
border: 1px solid rgba(246, 223, 163, 0.34);
|
||||
border: 1px solid var(--wg-etched-highlight);
|
||||
background-image: linear-gradient(
|
||||
160deg,
|
||||
rgba(42, 18, 34, 0.42),
|
||||
rgba(9, 8, 11, 0.58)
|
||||
);
|
||||
filter: saturate(0.62) contrast(1.12);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 242, 204, 0.28),
|
||||
inset 0 0 0 1px rgba(18, 15, 22, 0.62),
|
||||
@@ -272,7 +284,7 @@ ul.category-children {
|
||||
rgba(30, 28, 34, 0.62) 46%,
|
||||
rgba(36, 18, 29, 0.78)
|
||||
);
|
||||
border: 1px solid rgba(246, 223, 163, 0.3);
|
||||
border: 1px solid var(--wg-etched-highlight);
|
||||
border-radius: 4px !important;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 240, 190, 0.28),
|
||||
@@ -311,6 +323,7 @@ ul.category-children {
|
||||
|
||||
li[component="categories/category"] .meta.stats > div {
|
||||
min-height: 3.55rem;
|
||||
padding: var(--wg-space-2xs) var(--wg-space-xs);
|
||||
background: rgba(255, 255, 255, 0.018) !important;
|
||||
border: 1px solid rgba(194, 163, 90, 0.09) !important;
|
||||
border-radius: 3px !important;
|
||||
|
||||
@@ -45,6 +45,40 @@
|
||||
--bs-btn-active-border-color: rgba(216, 194, 138, 0.56);
|
||||
}
|
||||
|
||||
// core's guest sign-up prompt ships btn-warning/btn-info: remap to the
|
||||
// theme's two button voices (dark gold plate + ghost)
|
||||
.guest-cta-alert .btn-warning {
|
||||
--bs-btn-bg: #5a4a1a;
|
||||
--bs-btn-border-color: #a8893f;
|
||||
--bs-btn-color: #f0e6d8;
|
||||
--bs-btn-hover-bg: #6a5720;
|
||||
--bs-btn-hover-border-color: #c2a35a;
|
||||
--bs-btn-hover-color: #fff4dd;
|
||||
--bs-btn-active-bg: #493914;
|
||||
--bs-btn-active-border-color: #c2a35a;
|
||||
--bs-btn-active-color: #fff4dd;
|
||||
background-image: linear-gradient(to bottom, #5a4a1a 0%, #473914 100%);
|
||||
}
|
||||
|
||||
.guest-cta-alert .btn-info {
|
||||
--bs-btn-color: #d8cec0;
|
||||
--bs-btn-bg: rgba(8, 7, 10, 0.12);
|
||||
--bs-btn-border-color: rgba(216, 194, 138, 0.28);
|
||||
--bs-btn-hover-color: var(--wg-text);
|
||||
--bs-btn-hover-bg: rgba(194, 163, 90, 0.075);
|
||||
--bs-btn-hover-border-color: rgba(216, 194, 138, 0.44);
|
||||
--bs-btn-active-bg: rgba(194, 163, 90, 0.12);
|
||||
--bs-btn-active-border-color: rgba(216, 194, 138, 0.5);
|
||||
--bs-btn-active-color: var(--wg-text);
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.guest-cta-alert .btn-close {
|
||||
filter: invert(86%) sepia(16%) saturate(360%) hue-rotate(356deg)
|
||||
brightness(95%) contrast(90%);
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.modal-footer .btn-secondary,
|
||||
.bootbox .modal-footer .btn-secondary,
|
||||
.modal-footer .btn[data-bs-dismiss="modal"],
|
||||
@@ -207,20 +241,30 @@ textarea[component="topic/quickreply/text"]::placeholder {
|
||||
color: rgba(185, 178, 166, 0.62) !important;
|
||||
}
|
||||
|
||||
table[component="notification/table"] {
|
||||
table[component="notification/table"],
|
||||
.page-flags [component="flags/list"],
|
||||
li[component="post"] .content table {
|
||||
--bs-table-bg: transparent;
|
||||
--bs-table-color: var(--wg-text-soft);
|
||||
--bs-table-border-color: rgba(194, 163, 90, 0.1);
|
||||
--bs-table-border-color: rgba(194, 163, 90, 0.16);
|
||||
--bs-table-striped-bg: rgba(194, 163, 90, 0.035);
|
||||
--bs-table-striped-color: var(--wg-text-soft);
|
||||
--bs-table-hover-bg: rgba(194, 163, 90, 0.04);
|
||||
--bs-table-hover-color: var(--wg-text);
|
||||
color: var(--wg-text-soft) !important;
|
||||
}
|
||||
|
||||
// classless markdown tables in posts don't get Bootstrap's .table variables
|
||||
li[component="post"] .content table :where(th, td) {
|
||||
border-color: rgba(194, 163, 90, 0.16);
|
||||
}
|
||||
|
||||
table[component="notification/table"] :where(th, td, label) {
|
||||
color: var(--wg-text-soft) !important;
|
||||
}
|
||||
|
||||
table[component="notification/table"] thead :where(th, td) {
|
||||
table[component="notification/table"] thead :where(th, td),
|
||||
li[component="post"] .content table thead :where(th, td) {
|
||||
color: var(--wg-ledger-ink-muted) !important;
|
||||
font-family: var(--wg-font-display);
|
||||
font-size: 0.86rem;
|
||||
@@ -355,7 +399,7 @@ table[component="notification/table"] a[data-type]:focus {
|
||||
),
|
||||
linear-gradient(100deg, rgba(32, 19, 30, 0.98), rgba(13, 12, 17, 0.98)) !important;
|
||||
background-size:
|
||||
100% 1.82rem,
|
||||
100% var(--wg-ledger-line),
|
||||
auto;
|
||||
border-color: rgba(194, 163, 90, 0.18) !important;
|
||||
color: var(--wg-text-soft) !important;
|
||||
@@ -622,6 +666,6 @@ table[component="notification/table"] a[data-type]:focus {
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--wg-focus) !important;
|
||||
outline: 2px solid var(--wg-focus-ring) !important;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
+15
-23
@@ -33,7 +33,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.6fr) minmax(10rem, 1fr) minmax(10rem, 1fr);
|
||||
gap: clamp(2rem, 5vw, 3.125rem);
|
||||
width: min(100%, 1240px);
|
||||
width: min(100%, var(--wg-page-width));
|
||||
margin: 0 auto;
|
||||
padding: clamp(2.75rem, 6vw, 4rem) clamp(1rem, 4vw, 2rem) 1.875rem;
|
||||
}
|
||||
@@ -120,14 +120,14 @@
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
width: min(100%, 1240px);
|
||||
width: min(100%, var(--wg-page-width));
|
||||
margin: 0 auto;
|
||||
padding: 1.375rem clamp(1rem, 4vw, 2rem);
|
||||
border-top: 1px solid rgba(194, 163, 90, 0.1);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #7f756f;
|
||||
color: var(--wg-text-muted);
|
||||
font-family: var(--wg-font-ui);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
@@ -140,7 +140,7 @@
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.625rem;
|
||||
width: min(100%, 1240px);
|
||||
width: min(100%, var(--wg-page-width));
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem 1.875rem;
|
||||
font-family: var(--wg-font-ui);
|
||||
@@ -148,6 +148,7 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// quiet ghost credits — third-party names must never be the loudest pixels
|
||||
.wg-footer .wg-footer__powered-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -155,46 +156,37 @@
|
||||
gap: 0.45rem;
|
||||
width: 10.875rem;
|
||||
min-height: 2rem;
|
||||
border: 1px solid var(--wg-border);
|
||||
border: 1px solid var(--wg-border-soft);
|
||||
border-radius: 8px;
|
||||
padding: 0.42rem 0.75rem;
|
||||
color: var(--wg-text-muted);
|
||||
background: var(--wg-velvet-panel);
|
||||
box-shadow: var(--wg-velvet-shadow);
|
||||
background: rgba(255, 244, 221, 0.02);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: rgba(194, 163, 90, 0.22);
|
||||
color: #efe7da;
|
||||
background: var(--wg-velvet-panel-hover);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.035),
|
||||
inset 0 0 38px rgba(120, 36, 84, 0.14),
|
||||
0 12px 30px rgba(0, 0, 0, 0.3);
|
||||
color: var(--wg-text-soft);
|
||||
background: rgba(194, 163, 90, 0.06);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.035),
|
||||
inset 0 0 38px rgba(120, 36, 84, 0.14),
|
||||
0 0 0 2px rgba(194, 163, 90, 0.18),
|
||||
0 12px 30px rgba(0, 0, 0, 0.3);
|
||||
outline: 2px solid var(--wg-focus-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.wg-footer .wg-footer__powered-label {
|
||||
color: var(--wg-ledger-ink);
|
||||
color: var(--wg-text-muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wg-footer .wg-footer__powered-name {
|
||||
color: #f2edf7;
|
||||
font-family: var(--wg-font-display);
|
||||
color: var(--wg-text-soft);
|
||||
font-family: var(--wg-font-ui);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -46,9 +46,9 @@ li[component="post"] .post-container {
|
||||
background:
|
||||
linear-gradient(to bottom, transparent calc(100% - 1px), var(--wg-ledger-ruling) calc(100% - 1px)),
|
||||
var(--wg-ledger-panel) !important;
|
||||
background-size: 100% 1.82rem, auto;
|
||||
background-size: 100% var(--wg-ledger-line), auto;
|
||||
border: 1px solid var(--wg-ledger-border) !important;
|
||||
border-left: 2px solid rgba(194, 163, 90, 0.32) !important;
|
||||
border-left: 1px solid rgba(194, 163, 90, 0.32) !important;
|
||||
border-radius: 6px !important;
|
||||
box-shadow: var(--wg-ledger-shadow) !important;
|
||||
}
|
||||
@@ -101,6 +101,23 @@ li[component="post"] .content blockquote,
|
||||
|
||||
li[component="post"] .content {
|
||||
padding-top: 0.35rem;
|
||||
max-width: 82ch;
|
||||
}
|
||||
|
||||
// client.js wraps wide post tables in a focusable scroll container
|
||||
li[component="post"] .content .wg-mobile-table-scroll {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-color: rgba(194, 163, 90, 0.5) rgba(8, 7, 10, 0.48);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
li[component="post"] .content .wg-mobile-table-scroll > table {
|
||||
min-width: var(--wg-mobile-table-min-width, max-content);
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
li[component="post"] .content h1,
|
||||
@@ -224,7 +241,7 @@ li[component="post"] [component="post/downvote"] {
|
||||
background:
|
||||
linear-gradient(to bottom, transparent calc(100% - 1px), rgba(194, 163, 90, 0.08) calc(100% - 1px)),
|
||||
rgba(18, 15, 22, 0.96) !important;
|
||||
background-size: 100% 1.55rem, auto;
|
||||
background-size: 100% var(--wg-ledger-line), auto;
|
||||
border-color: var(--wg-ledger-border) !important;
|
||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.32) !important;
|
||||
color: var(--wg-ledger-ink) !important;
|
||||
@@ -258,7 +275,7 @@ textarea[component="topic/quickreply/text"],
|
||||
background:
|
||||
linear-gradient(to bottom, transparent calc(100% - 1px), var(--wg-ledger-ruling) calc(100% - 1px)),
|
||||
rgba(18, 15, 22, 0.92) !important;
|
||||
background-size: 100% 1.82rem, auto;
|
||||
background-size: 100% var(--wg-ledger-line), auto;
|
||||
border-color: var(--wg-ledger-border) !important;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.026),
|
||||
@@ -283,7 +300,7 @@ textarea[component="topic/quickreply/text"]:focus,
|
||||
background:
|
||||
linear-gradient(to bottom, transparent calc(100% - 1px), rgba(194, 163, 90, 0.05) calc(100% - 1px)),
|
||||
var(--wg-ledger-panel) !important;
|
||||
background-size: 100% 1.82rem, auto;
|
||||
background-size: 100% var(--wg-ledger-line), auto;
|
||||
border-color: var(--wg-ledger-border) !important;
|
||||
box-shadow: none !important;
|
||||
color: var(--wg-text-soft) !important;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
li[component="categories/category"],
|
||||
li[component="category/topic"],
|
||||
li[component="post"] .post-container {
|
||||
padding: 0.85rem !important;
|
||||
padding: var(--wg-space-sm) !important;
|
||||
}
|
||||
|
||||
li[component="categories/category"] h2.title {
|
||||
@@ -29,11 +29,17 @@
|
||||
.topic-list-header [component="category/controls"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// keep the guest "Log in to post / reply" CTA on one line
|
||||
[component="category/post/guest"],
|
||||
[component="topic/reply/guest"] {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
.category-header {
|
||||
padding: 0.85rem;
|
||||
padding: var(--wg-space-sm);
|
||||
}
|
||||
|
||||
li[component="post"] .content,
|
||||
|
||||
@@ -216,6 +216,40 @@ li[component="category/topic"] .user-icon {
|
||||
0 0 10px rgba(0, 0, 0, 0.34);
|
||||
}
|
||||
|
||||
// user data (email, join/last-online dates) never wears the engraving —
|
||||
// Harmony marks these with .text-break + .ff-secondary in the stats row
|
||||
.account-stats .text-break.ff-secondary {
|
||||
font-family: var(--wg-font-text);
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
// 404: core picks a random beige animal glyph (dog, hippo, otter, ...);
|
||||
// match the glyph element itself so every variant wears the cowl
|
||||
.template-404 i.fa.fa-4x {
|
||||
display: inline-block;
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
background-color: var(--wg-gold-soft);
|
||||
-webkit-mask: url("./plugins/nodebb-theme-westgate/game-icons/cowled.svg")
|
||||
center / contain no-repeat;
|
||||
mask: url("./plugins/nodebb-theme-westgate/game-icons/cowled.svg")
|
||||
center / contain no-repeat;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
// replace core's grey-triangle default cover with the velvet material
|
||||
// (.cover on profiles, .list-cover on the groups list)
|
||||
.cover[style*="cover-default.png"],
|
||||
.list-cover[style*="cover-default.png"] {
|
||||
background-image:
|
||||
radial-gradient(120% 90% at 18% 0%, rgba(58, 24, 48, 0.6), transparent 60%),
|
||||
linear-gradient(100deg, #2a1222, #110f15 55%, #09080b) !important;
|
||||
}
|
||||
|
||||
.account,
|
||||
.account .profile,
|
||||
.account .cover,
|
||||
@@ -357,15 +391,7 @@ li[component="category/topic"] .user-icon {
|
||||
border-bottom-color: rgba(194, 163, 90, 0.12) !important;
|
||||
}
|
||||
|
||||
.page-flags [component="flags/list"] {
|
||||
--bs-table-bg: transparent;
|
||||
--bs-table-color: var(--wg-text-soft);
|
||||
--bs-table-border-color: rgba(194, 163, 90, 0.1);
|
||||
--bs-table-hover-bg: rgba(194, 163, 90, 0.04);
|
||||
--bs-table-hover-color: var(--wg-text);
|
||||
color: var(--wg-text-soft) !important;
|
||||
}
|
||||
|
||||
// table variables shared with notification/post tables live in _controls.scss
|
||||
.page-flags [component="flags/list"] :where(th, td) {
|
||||
color: var(--wg-text-soft) !important;
|
||||
border-color: rgba(194, 163, 90, 0.1) !important;
|
||||
|
||||
@@ -71,6 +71,14 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--wg-space-2xs: 0.25rem;
|
||||
--wg-space-xs: 0.5rem;
|
||||
--wg-space-sm: 0.75rem;
|
||||
--wg-space-md: 1rem;
|
||||
--wg-space-lg: 1.5rem;
|
||||
--wg-space-xl: 2rem;
|
||||
--wg-ledger-line: 1.78rem;
|
||||
--wg-page-width: 1420px;
|
||||
--wg-font-display: "Cinzel", Georgia, "Times New Roman", serif;
|
||||
--wg-font-text: "Jost", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
||||
--wg-font-ui: "Jost", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
||||
@@ -96,6 +104,7 @@
|
||||
--wg-border: rgba(194, 163, 90, 0.14);
|
||||
--wg-border-soft: rgba(194, 163, 90, 0.08);
|
||||
--wg-focus: rgba(194, 163, 90, 0.28);
|
||||
--wg-focus-ring: #c2a35a;
|
||||
--wg-ledger-panel: linear-gradient(
|
||||
100deg,
|
||||
rgba(38, 22, 35, 0.5) 0%,
|
||||
|
||||
@@ -28,7 +28,7 @@ $wg-topbar-panel-bg: linear-gradient(
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.9rem;
|
||||
width: min(100%, 1420px);
|
||||
width: min(100%, var(--wg-page-width));
|
||||
min-height: 64px;
|
||||
margin: 0 auto;
|
||||
padding: 0.55rem clamp(0.85rem, 2vw, 1.5rem);
|
||||
@@ -200,11 +200,24 @@ $wg-topbar-panel-bg: linear-gradient(
|
||||
background: rgba(194, 163, 90, 0.08) !important;
|
||||
}
|
||||
|
||||
// one primary vocabulary: the dark gold plate from _controls.scss
|
||||
.wg-topbar .btn-primary,
|
||||
.wg-topbar__register {
|
||||
border-color: rgba(194, 163, 90, 0.34) !important;
|
||||
color: #1b1208 !important;
|
||||
background: linear-gradient(to bottom, #d8c28a, var(--wg-gold)) !important;
|
||||
border: 1px solid var(--wg-gold-soft) !important;
|
||||
color: #f0e6d8 !important;
|
||||
background: linear-gradient(to bottom, #5a4a1a, #473914) !important;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.06),
|
||||
0 2px 8px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.wg-topbar .btn-primary:hover,
|
||||
.wg-topbar .btn-primary:focus,
|
||||
.wg-topbar__register:hover,
|
||||
.wg-topbar__register:focus {
|
||||
border-color: var(--wg-gold) !important;
|
||||
color: #fff4dd !important;
|
||||
background: linear-gradient(to bottom, #6a5720, #53431a) !important;
|
||||
}
|
||||
|
||||
.wg-topbar__login,
|
||||
|
||||
+28
-14
@@ -20,7 +20,7 @@ body.template-category .category-header .description {
|
||||
}
|
||||
|
||||
.topic-list-header > .card {
|
||||
padding: 0.55rem !important;
|
||||
padding: var(--wg-space-xs) !important;
|
||||
background: rgba(18, 15, 22, 0.78) !important;
|
||||
border-color: var(--wg-ledger-border) !important;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.018) !important;
|
||||
@@ -42,7 +42,7 @@ body.template-category .category-header .description {
|
||||
|
||||
ul[component="category"] {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
gap: var(--wg-space-sm);
|
||||
padding: 0 !important;
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
@@ -53,15 +53,15 @@ li[component="category/topic"] {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
margin: 0 !important;
|
||||
padding: 0.95rem 1rem !important;
|
||||
padding: var(--wg-space-md) !important;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(to bottom, transparent calc(100% - 1px), var(--wg-ledger-ruling) calc(100% - 1px)),
|
||||
linear-gradient(to right, rgba(194, 163, 90, 0.08), transparent 5.25rem),
|
||||
var(--wg-ledger-panel) !important;
|
||||
background-size: 100% 1.78rem, auto, auto;
|
||||
background-size: 100% var(--wg-ledger-line), auto, auto;
|
||||
border: 1px solid var(--wg-ledger-border) !important;
|
||||
border-left: 2px solid rgba(194, 163, 90, 0.34) !important;
|
||||
border-left: 1px solid rgba(194, 163, 90, 0.34) !important;
|
||||
border-radius: 6px !important;
|
||||
box-shadow: var(--wg-ledger-shadow) !important;
|
||||
transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
|
||||
@@ -98,7 +98,7 @@ li[component="category/topic"].unread:nth-child(even) {
|
||||
linear-gradient(to bottom, transparent calc(100% - 1px), rgba(216, 194, 138, 0.055) calc(100% - 1px)),
|
||||
linear-gradient(to right, rgba(216, 194, 138, 0.09), transparent 5.25rem),
|
||||
var(--wg-ledger-panel) !important;
|
||||
background-size: 100% 1.78rem, auto, auto;
|
||||
background-size: 100% var(--wg-ledger-line), auto, auto;
|
||||
}
|
||||
|
||||
li[component="category/topic"]:hover {
|
||||
@@ -113,9 +113,7 @@ li[component="category/topic"].unread {
|
||||
var(--wg-ledger-panel) !important;
|
||||
border-color: rgba(216, 194, 138, 0.17) !important;
|
||||
border-left-color: var(--wg-ledger-ink) !important;
|
||||
box-shadow:
|
||||
var(--wg-ledger-shadow),
|
||||
inset 2px 0 0 rgba(216, 194, 138, 0.1) !important;
|
||||
box-shadow: var(--wg-ledger-shadow) !important;
|
||||
}
|
||||
|
||||
li[component="category/topic"].pinned {
|
||||
@@ -139,7 +137,7 @@ li[component="category/topic"].pinned:not(.unread) {
|
||||
}
|
||||
|
||||
li[component="category/topic"].pinned:has(+ li[component="category/topic"]:not(.pinned)) {
|
||||
margin-bottom: 0.85rem !important;
|
||||
margin-bottom: var(--wg-space-sm) !important;
|
||||
}
|
||||
|
||||
li[component="category/topic"].pinned:has(+ li[component="category/topic"]:not(.pinned))::after {
|
||||
@@ -161,8 +159,7 @@ li[component="category/topic"].locked {
|
||||
border-left-color: rgba(168, 74, 78, 0.42) !important;
|
||||
box-shadow:
|
||||
var(--wg-ledger-shadow),
|
||||
0 0 12px rgba(142, 52, 56, 0.09),
|
||||
inset 2px 0 0 rgba(142, 52, 56, 0.1) !important;
|
||||
0 0 12px rgba(142, 52, 56, 0.09) !important;
|
||||
}
|
||||
|
||||
li[component="category/topic"].locked:not(.unread) {
|
||||
@@ -182,8 +179,7 @@ li[component="category/topic"].locked.pinned {
|
||||
border-left-color: rgba(168, 74, 78, 0.58) !important;
|
||||
box-shadow:
|
||||
var(--wg-ledger-shadow),
|
||||
0 0 14px rgba(142, 52, 56, 0.085),
|
||||
inset 2px 0 0 rgba(216, 194, 138, 0.13) !important;
|
||||
0 0 14px rgba(142, 52, 56, 0.085) !important;
|
||||
}
|
||||
|
||||
li[component="category/topic"].pinned::after,
|
||||
@@ -275,6 +271,12 @@ li[component="category/topic"] [component="topic/labels"] .badge {
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
// timestamps are data, not labels — the Engraving Rule keeps them plain
|
||||
li[component="category/topic"] [component="topic/labels"] .badge.timeago {
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
li[component="category/topic"] [component="topic/labels"] .badge i {
|
||||
color: var(--wg-ledger-ink);
|
||||
margin-right: 0.12rem;
|
||||
@@ -364,6 +366,7 @@ li[component="category/topic"] [component="topic/select"]:focus {
|
||||
|
||||
li[component="category/topic"] .meta.stats > div {
|
||||
min-height: 3.25rem;
|
||||
padding: var(--wg-space-2xs) var(--wg-space-xs);
|
||||
background: rgba(255, 255, 255, 0.018) !important;
|
||||
border: 1px solid rgba(194, 163, 90, 0.09) !important;
|
||||
border-radius: 3px !important;
|
||||
@@ -395,6 +398,17 @@ li[component="category/topic"] [component="topic/teaser"] .lastpost {
|
||||
color: var(--wg-ledger-ink-muted);
|
||||
}
|
||||
|
||||
// Harmony marks teasers with .border-2 + .lh-sm: keep the gilt edge at 1px
|
||||
// and give the clamped teaser text breathing room
|
||||
.lastpost.border-2 {
|
||||
border-width: 1px !important;
|
||||
}
|
||||
|
||||
.lastpost.lh-sm,
|
||||
.lastpost .lh-sm {
|
||||
line-height: 1.35 !important;
|
||||
}
|
||||
|
||||
li[component="category/topic"] [component="topic/teaser"] .permalink,
|
||||
li[component="category/topic"] [component="topic/teaser"] .timeago {
|
||||
color: var(--wg-ledger-ink-muted) !important;
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
[data-widget-area="header"],
|
||||
[data-widget-area="footer"] {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
gap: var(--wg-space-sm);
|
||||
}
|
||||
|
||||
[data-widget-area="header"] {
|
||||
margin-bottom: 0.85rem;
|
||||
margin-bottom: var(--wg-space-sm);
|
||||
}
|
||||
|
||||
[data-widget-area="footer"] {
|
||||
@@ -20,7 +20,7 @@
|
||||
[data-widget-area="sidebar-footer"] {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 0.85rem;
|
||||
gap: var(--wg-space-sm);
|
||||
}
|
||||
|
||||
[data-widget-area] > :where(
|
||||
@@ -51,11 +51,11 @@
|
||||
.westgate-widget-card,
|
||||
.wg-widget-card
|
||||
) {
|
||||
padding: 0.85rem;
|
||||
padding: var(--wg-space-md);
|
||||
background:
|
||||
linear-gradient(to bottom, transparent calc(100% - 1px), rgba(194, 163, 90, 0.045) calc(100% - 1px)),
|
||||
linear-gradient(100deg, rgba(42, 18, 34, 0.34), rgba(18, 16, 23, 0.96) 62%, rgba(13, 12, 17, 0.98)) !important;
|
||||
background-size: 100% 1.78rem, auto;
|
||||
background-size: 100% var(--wg-ledger-line), auto;
|
||||
border: 1px solid rgba(194, 163, 90, 0.14);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--wg-velvet-shadow);
|
||||
@@ -229,7 +229,7 @@
|
||||
--bs-gutter-x: 0.45rem;
|
||||
--bs-gutter-y: 0.45rem;
|
||||
width: 100%;
|
||||
margin: 0 0 0.85rem;
|
||||
margin: 0 0 var(--wg-space-sm);
|
||||
padding: 0.6rem;
|
||||
background:
|
||||
linear-gradient(to bottom, rgba(255, 241, 196, 0.018), rgba(8, 7, 10, 0.08)),
|
||||
|
||||
@@ -1216,7 +1216,7 @@
|
||||
}
|
||||
|
||||
.westgate-wiki .wiki-article-prose .wg-mobile-table-scroll:focus {
|
||||
outline: 2px solid rgba(194, 163, 90, 0.42);
|
||||
outline: 2px solid var(--wg-focus-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
|
||||
@@ -105,10 +105,6 @@
|
||||
</a>
|
||||
{{{ end }}}
|
||||
{{{ end }}}
|
||||
<a class="wg-topbar__drawer-link" href="{relative_path}/categories"><i class="fa fa-fw fa-layer-group" aria-hidden="true"></i><span>[[pages:categories]]</span></a>
|
||||
<a class="wg-topbar__drawer-link" href="{relative_path}/recent"><i class="fa fa-fw fa-clock" aria-hidden="true"></i><span>[[recent:title]]</span></a>
|
||||
<a class="wg-topbar__drawer-link" href="{relative_path}/unread"><i class="fa fa-fw fa-inbox" aria-hidden="true"></i><span>[[unread:title]]</span></a>
|
||||
<a class="wg-topbar__drawer-link" href="{relative_path}/tags"><i class="fa fa-fw fa-tags" aria-hidden="true"></i><span>[[tags:tags]]</span></a>
|
||||
</nav>
|
||||
|
||||
{{{ if config.loggedIn }}}
|
||||
|
||||
@@ -53,7 +53,7 @@ assertIncludes(partial, 'aria-label="Powered by Tiptap"', 'Tiptap credit should
|
||||
'{relative_path}/category/1/news',
|
||||
'{relative_path}/gallery',
|
||||
'{relative_path}/category/84/developer-blog',
|
||||
'{relative_path}/join-the-team',
|
||||
'{relative_path}/recruitment',
|
||||
'{relative_path}/categories',
|
||||
'{relative_path}/wiki',
|
||||
'{relative_path}/register',
|
||||
|
||||
Reference in New Issue
Block a user