prototype(#54): ornament and frame component set as a specimen page
Throwaway. Names the closed component vocabulary (rail, panel, plaque, band, ledger, four dividers, section header, icon, horizon, stamp) and shows it at both densities with zero images, plus the same components mapped onto the live category list and topic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@@ -0,0 +1,27 @@
|
|||||||
|
# Throwaway prototype for #54 — the ornament and frame component set
|
||||||
|
|
||||||
|
Not for merge. Nothing here is theme code; it is the named component set made
|
||||||
|
concrete so it can be looked at.
|
||||||
|
|
||||||
|
- `ornament.css` — the component set itself, standalone, zero images.
|
||||||
|
- `specimen.html` — every named component, at both densities, on one page.
|
||||||
|
- `real-surface.css` — the same components mapped onto real NodeBB markup.
|
||||||
|
- `shots.js` — before/after on the live category list and a 13-post topic.
|
||||||
|
- `shots/` — the images the resolution is judged on.
|
||||||
|
|
||||||
|
The palette is candidate B from #53, injected at runtime (`../_base-plumnoir.css`
|
||||||
|
plus `../candidate-b.css`), so no rebuild is needed to see it.
|
||||||
|
|
||||||
|
Replay:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# specimen page — needs http, not file://: mask-image on an SVG is blocked by CORS
|
||||||
|
python3 -m http.server 8931 # then open /prototype/ornament/specimen.html
|
||||||
|
|
||||||
|
# real surface
|
||||||
|
cd ../sow-nodebb && nix develop --command make dev-reset && nix develop --command make dev
|
||||||
|
cd - && node prototype/seed.js
|
||||||
|
export PLAYWRIGHT_BROWSERS_PATH=$(nix build --print-out-paths --no-link nixpkgs#playwright-driver.browsers)
|
||||||
|
P=$(nix build --print-out-paths --no-link nixpkgs#playwright-test)
|
||||||
|
NODE_PATH=$P/lib/node_modules nix shell nixpkgs#playwright-test -c node prototype/ornament/shots.js
|
||||||
|
```
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
/* #54 — the ornament and frame component set, as a closed vocabulary.
|
||||||
|
Throwaway. Zero images: every mark here is CSS. Palette = candidate B (#53). */
|
||||||
|
|
||||||
|
/* ---- palette B (from prototype/_base-plumnoir.css + candidate-b.css) ---- */
|
||||||
|
:root {
|
||||||
|
--wg-bg: #150e13;
|
||||||
|
--wg-bg-deep: #0c0810;
|
||||||
|
--wg-panel: #21161c;
|
||||||
|
--wg-panel-2: #180f16;
|
||||||
|
--wg-panel-3: #351e28;
|
||||||
|
--wg-plum: #351e28;
|
||||||
|
--wg-plum-soft: #4a2a38;
|
||||||
|
|
||||||
|
--wg-gold: #c2a35a;
|
||||||
|
--wg-gold-rgb: 194, 163, 90;
|
||||||
|
--wg-gold-lit: #e0c878;
|
||||||
|
--wg-old-gilt: #a8893f;
|
||||||
|
--wg-ledger-ink: #d8c28a;
|
||||||
|
--wg-red: #8e3438;
|
||||||
|
--wg-stamp: #a84a4e;
|
||||||
|
|
||||||
|
--wg-pewter: #9ea7b3;
|
||||||
|
--wg-pewter-rgb: 158, 167, 179;
|
||||||
|
|
||||||
|
--wg-text: #e6e0d6;
|
||||||
|
--wg-text-soft: #b9b2a6;
|
||||||
|
--wg-text-muted: #9ea7b3;
|
||||||
|
|
||||||
|
/* the four dividers, as tokens — this list is closed */
|
||||||
|
--wg-ruling: rgba(var(--wg-pewter-rgb), 0.075);
|
||||||
|
--wg-edge: rgba(var(--wg-pewter-rgb), 0.2);
|
||||||
|
--wg-edge-soft: rgba(var(--wg-pewter-rgb), 0.11);
|
||||||
|
|
||||||
|
--wg-page-width: 1420px;
|
||||||
|
--wg-ledger-line: 1.78rem;
|
||||||
|
--wg-font-display: Cinzel, Georgia, serif;
|
||||||
|
--wg-font-body: Jost, system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================================
|
||||||
|
1. RAIL — not decoration, a constraint. One left edge, one right edge,
|
||||||
|
every page, every density (#49 One Rail).
|
||||||
|
====================================================================== */
|
||||||
|
.wg-rail {
|
||||||
|
max-width: var(--wg-page-width);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding-inline: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================================
|
||||||
|
2. PANEL — the bounded surface. Two weights, nothing between.
|
||||||
|
Edges Fade (#49): the edge is a masked overlay, strongest at the top,
|
||||||
|
gone by the bottom. No hairline runs corner to corner.
|
||||||
|
====================================================================== */
|
||||||
|
.wg-panel {
|
||||||
|
position: relative;
|
||||||
|
background: var(--wg-panel);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: var(--wg-pad, 1rem);
|
||||||
|
}
|
||||||
|
.wg-panel::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
border: 1px solid var(--wg-edge);
|
||||||
|
-webkit-mask-image: linear-gradient(180deg, #000, rgba(0, 0, 0, 0.12));
|
||||||
|
mask-image: linear-gradient(180deg, #000, rgba(0, 0, 0, 0.12));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
/* cheap panel: the same shape one step down. No sheen, no fade — flat. */
|
||||||
|
.wg-panel--plain {
|
||||||
|
background: var(--wg-panel-2);
|
||||||
|
}
|
||||||
|
.wg-panel--plain::before {
|
||||||
|
border-color: var(--wg-edge-soft);
|
||||||
|
-webkit-mask-image: none;
|
||||||
|
mask-image: none;
|
||||||
|
}
|
||||||
|
/* velvet sheen — texture #1. Inset only, never an asset. */
|
||||||
|
.wg-panel--velvet {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
.wg-panel--velvet:hover {
|
||||||
|
background: color-mix(in srgb, var(--wg-panel) 88%, var(--wg-plum-soft));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================================
|
||||||
|
3. PLAQUE — a name plate, not a band. inline-block is the whole idea.
|
||||||
|
Page titles and standing labels. Suppressed where the logo does the job.
|
||||||
|
====================================================================== */
|
||||||
|
.wg-plaque {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--wg-bg-deep);
|
||||||
|
border: 1px solid var(--wg-edge);
|
||||||
|
padding: 0.35rem 1rem;
|
||||||
|
font-family: var(--wg-font-display);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.86rem;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--wg-ledger-ink);
|
||||||
|
}
|
||||||
|
.wg-plaque--gilt {
|
||||||
|
border-color: rgba(var(--wg-gold-rgb), 0.34);
|
||||||
|
}
|
||||||
|
/* the straddling legend: a title sitting on its own panel edge */
|
||||||
|
.wg-legend {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.wg-legend > .wg-plaque {
|
||||||
|
position: absolute;
|
||||||
|
top: -1.05rem;
|
||||||
|
left: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================================
|
||||||
|
4. BAND — a strip that spans the rail, edge to edge. Never inset.
|
||||||
|
Shell chrome only: topbar, lintel, footer, thresholds.
|
||||||
|
====================================================================== */
|
||||||
|
.wg-band {
|
||||||
|
position: relative;
|
||||||
|
background: var(--wg-panel-2);
|
||||||
|
padding-block: 0.75rem;
|
||||||
|
}
|
||||||
|
/* LINTEL — divider #3. Gilt rule that bleeds light downward (#48 Warframe). */
|
||||||
|
.wg-band--lintel {
|
||||||
|
border-top: 1px solid rgba(var(--wg-gold-rgb), 0.5);
|
||||||
|
background-image: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(var(--wg-gold-rgb), 0.1),
|
||||||
|
transparent 32px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================================
|
||||||
|
5. LEDGER — the archive interior. Ruled rows, one line height.
|
||||||
|
====================================================================== */
|
||||||
|
.wg-ledger {
|
||||||
|
background: var(--wg-panel-2);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.wg-ledger__row {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: baseline;
|
||||||
|
min-height: calc(var(--wg-ledger-line) * 1.6);
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
/* RULING — divider #1. Row separation, nothing else. */
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent calc(100% - 1px),
|
||||||
|
var(--wg-ruling) calc(100% - 1px)
|
||||||
|
);
|
||||||
|
/* gilt left mark: this row has something at stake (unread, owned) */
|
||||||
|
border-left: 1px solid transparent;
|
||||||
|
}
|
||||||
|
.wg-ledger__row--marked {
|
||||||
|
border-left-color: rgba(var(--wg-gold-rgb), 0.34);
|
||||||
|
}
|
||||||
|
.wg-ledger__row:last-child {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================================
|
||||||
|
DIVIDERS — the closed list. Four, and no fifth.
|
||||||
|
1. ruling (above, .wg-ledger__row)
|
||||||
|
2. fading rule
|
||||||
|
3. lintel (above, .wg-band--lintel)
|
||||||
|
4. spine
|
||||||
|
====================================================================== */
|
||||||
|
.wg-rule {
|
||||||
|
height: 1px;
|
||||||
|
border: 0;
|
||||||
|
background: linear-gradient(90deg, var(--wg-edge), transparent);
|
||||||
|
}
|
||||||
|
.wg-spine {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
}
|
||||||
|
.wg-spine::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0.2rem;
|
||||||
|
bottom: 0.2rem;
|
||||||
|
width: 1px;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(var(--wg-gold-rgb), 0.18),
|
||||||
|
rgba(var(--wg-gold-rgb), 0.07)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================================
|
||||||
|
SECTION HEADER — one component, two forms.
|
||||||
|
A header earns ornament by switching face and case, not by gaining a
|
||||||
|
background or a flourish (#48 Warframe).
|
||||||
|
====================================================================== */
|
||||||
|
.wg-head {
|
||||||
|
font-family: var(--wg-font-body);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: none;
|
||||||
|
color: var(--wg-text-soft);
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
}
|
||||||
|
.wg-head--earned {
|
||||||
|
font-family: var(--wg-font-display);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--wg-ledger-ink);
|
||||||
|
}
|
||||||
|
/* the one permitted flourish, and only on an earned header: a rule that
|
||||||
|
starts at the text and fades out to the rail. */
|
||||||
|
.wg-head--earned.wg-head--ruled {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
.wg-head--earned.wg-head--ruled::after {
|
||||||
|
content: "";
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, rgba(var(--wg-gold-rgb), 0.34), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================================
|
||||||
|
ICON — mask over a token, never a coloured image.
|
||||||
|
Pewter structures, gilt marks value (#49 Two Metals).
|
||||||
|
====================================================================== */
|
||||||
|
.wg-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: var(--wg-icon-size, 20px);
|
||||||
|
height: var(--wg-icon-size, 20px);
|
||||||
|
background-color: var(--wg-pewter);
|
||||||
|
-webkit-mask: var(--wg-icon-src) center / contain no-repeat;
|
||||||
|
mask: var(--wg-icon-src) center / contain no-repeat;
|
||||||
|
vertical-align: -0.2em;
|
||||||
|
}
|
||||||
|
.wg-icon--gilt {
|
||||||
|
background-color: var(--wg-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================================
|
||||||
|
HORIZON — texture #2. The city at night instead of hero art.
|
||||||
|
Showcase only. Radial, warm, never cyan (#49 No Machines).
|
||||||
|
====================================================================== */
|
||||||
|
.wg-horizon {
|
||||||
|
background:
|
||||||
|
radial-gradient(
|
||||||
|
120% 80% at 50% 100%,
|
||||||
|
rgba(var(--wg-gold-rgb), 0.12),
|
||||||
|
transparent 62%
|
||||||
|
),
|
||||||
|
linear-gradient(180deg, var(--wg-bg-deep), var(--wg-bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================================
|
||||||
|
DENSITY — one shell, two densities. Ornament weight and row spacing
|
||||||
|
only. Width never changes (One Rail).
|
||||||
|
====================================================================== */
|
||||||
|
.wg-showcase {
|
||||||
|
--wg-pad: 1.5rem;
|
||||||
|
--wg-icon-size: 28px;
|
||||||
|
}
|
||||||
|
.wg-app {
|
||||||
|
--wg-pad: 0.75rem;
|
||||||
|
--wg-icon-size: 20px;
|
||||||
|
}
|
||||||
|
/* app density: sheen off, plaques off except the page title, headers reduced */
|
||||||
|
.wg-app .wg-panel--velvet {
|
||||||
|
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.42);
|
||||||
|
}
|
||||||
|
.wg-app .wg-band--lintel {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(var(--wg-gold-rgb), 0.06),
|
||||||
|
transparent 18px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ======================================================================
|
||||||
|
STAMP — the only red. A state or a mark of loss, never decoration.
|
||||||
|
====================================================================== */
|
||||||
|
.wg-stamp {
|
||||||
|
font-family: var(--wg-font-display);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--wg-stamp);
|
||||||
|
border: 1px solid rgba(168, 74, 78, 0.45);
|
||||||
|
padding: 0.1rem 0.5rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/* #54 — the component set applied to one real surface: the category list and
|
||||||
|
a topic, on the live stack. Injected at runtime over palette B. Throwaway.
|
||||||
|
|
||||||
|
Every rule below is one of the named components doing its job on real
|
||||||
|
NodeBB markup. Nothing new is invented here. */
|
||||||
|
|
||||||
|
/* ---- PANEL: edges fade. No hairline runs corner to corner. ---- */
|
||||||
|
li[component="categories/category"],
|
||||||
|
li[component="category/topic"],
|
||||||
|
.westgate-surface,
|
||||||
|
.card {
|
||||||
|
border-color: transparent !important;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
li[component="categories/category"]::after,
|
||||||
|
.westgate-surface::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
border: 1px solid var(--wg-border);
|
||||||
|
-webkit-mask-image: linear-gradient(180deg, #000, rgba(0, 0, 0, 0.12));
|
||||||
|
mask-image: linear-gradient(180deg, #000, rgba(0, 0, 0, 0.12));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- LEDGER: rows are ruled, not boxed. The category list is a ledger:
|
||||||
|
one surface, ruled rows, no gaps and no per-row card. ---- */
|
||||||
|
ul.categories-list,
|
||||||
|
ul.westgate-categories-list {
|
||||||
|
gap: 0 !important;
|
||||||
|
background: var(--wg-panel-2);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
ul.categories-list li[component="categories/category"],
|
||||||
|
ul.westgate-categories-list li[component="categories/category"] {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
background: none !important;
|
||||||
|
/* RULING — the only separator inside a ledger */
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent calc(100% - 1px),
|
||||||
|
var(--wg-ledger-ruling) calc(100% - 1px)
|
||||||
|
) !important;
|
||||||
|
}
|
||||||
|
ul.categories-list li[component="categories/category"]::after,
|
||||||
|
ul.westgate-categories-list li[component="categories/category"]::after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- SECTION HEADER: earned form = display face + uppercase + one colour
|
||||||
|
token + the single permitted flourish, a gilt rule fading to the rail. ---- */
|
||||||
|
li[component="categories/category"] h2.title,
|
||||||
|
.category-header h1,
|
||||||
|
[data-widget-area] .card-header {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.9rem;
|
||||||
|
font-family: var(--wg-font-display) !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
letter-spacing: 0.08em !important;
|
||||||
|
color: var(--wg-ledger-ink) !important;
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
li[component="categories/category"] h2.title::after,
|
||||||
|
.category-header h1::after {
|
||||||
|
content: "";
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, rgba(var(--wg-gold-rgb), 0.34), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- PLAQUE: the page title becomes a name plate, not a band. ---- */
|
||||||
|
body.template-category .category-header .category-title,
|
||||||
|
body.template-topic .topic-title h1 {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--wg-bg-deep);
|
||||||
|
border: 1px solid var(--wg-border);
|
||||||
|
padding: 0.35rem 1rem;
|
||||||
|
font-family: var(--wg-font-display);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--wg-ledger-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- ICON: pewter is structure. Gilt is reserved for what can be touched. ---- */
|
||||||
|
li[component="categories/category"] .icon [class*="wg-icon-"],
|
||||||
|
li[component="categories/category"] [class*="wg-icon-"] {
|
||||||
|
background-color: var(--wg-pewter) !important;
|
||||||
|
}
|
||||||
|
/* The category icon plate keeps its plum wash and desaturation: ACP colours
|
||||||
|
arrive candy-bright and the existing treatment is what pulls them into the
|
||||||
|
register. Only its ring moves from gilt to pewter — the plate structures,
|
||||||
|
it does not mark value. */
|
||||||
|
li[component="categories/category"] .icon {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.12),
|
||||||
|
inset 0 -3px 7px rgba(0, 0, 0, 0.52),
|
||||||
|
0 0 0 1px rgba(var(--wg-pewter-rgb), 0.24) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- LINTEL: the topbar is a threshold, and says so. ---- */
|
||||||
|
.wg-topbar,
|
||||||
|
#header-menu {
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
.wg-topbar {
|
||||||
|
border-top: 1px solid rgba(var(--wg-gold-rgb), 0.5) !important;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(var(--wg-gold-rgb), 0.1),
|
||||||
|
transparent 32px
|
||||||
|
) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- APP DENSITY: sheen off. Hierarchy is tone plus the two metals. ---- */
|
||||||
|
li[component="categories/category"],
|
||||||
|
li[component="category/topic"] {
|
||||||
|
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.42) !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Throwaway screenshotter for #54. Palette B is the ground; the only variable
|
||||||
|
// is whether the ornament component set is applied.
|
||||||
|
// PLAYWRIGHT_BROWSERS_PATH=... nix shell nixpkgs#playwright-test -c node prototype/ornament/shots.js
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { chromium } = require('playwright');
|
||||||
|
|
||||||
|
const BASE = process.env.BASE || 'http://localhost:4567';
|
||||||
|
const OUT = path.join(__dirname, 'shots');
|
||||||
|
const read = (f) => fs.readFileSync(path.join(__dirname, f), 'utf8');
|
||||||
|
const paletteB = read('../_base-plumnoir.css') + read('../candidate-b.css');
|
||||||
|
const ornament = read('real-surface.css');
|
||||||
|
|
||||||
|
const VARIANTS = { before: paletteB, after: paletteB + ornament };
|
||||||
|
const PAGES = [
|
||||||
|
['categories', '/categories'],
|
||||||
|
['topic', '/topic/2'],
|
||||||
|
];
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
fs.mkdirSync(OUT, { recursive: true });
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const ctx = await browser.newContext({ viewport: { width: 1600, height: 1000 }, deviceScaleFactor: 2 });
|
||||||
|
const page = await ctx.newPage();
|
||||||
|
|
||||||
|
await page.goto(BASE + '/login');
|
||||||
|
await page.fill('#username', 'admin');
|
||||||
|
await page.fill('#password', 'Admin12345!');
|
||||||
|
await page.click('#login');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
for (const [name, css] of Object.entries(VARIANTS)) {
|
||||||
|
for (const [pname, url] of PAGES) {
|
||||||
|
await page.goto(BASE + url, { waitUntil: 'networkidle' });
|
||||||
|
await page.addStyleTag({ content: css });
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await page.screenshot({ path: path.join(OUT, `${pname}-${name}.png`), fullPage: true });
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(OUT, `${pname}-${name}-crop.png`),
|
||||||
|
clip: { x: 120, y: 60, width: 1060, height: 620 },
|
||||||
|
});
|
||||||
|
console.log(pname, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await browser.close();
|
||||||
|
})();
|
||||||
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 555 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 633 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 966 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 961 KiB |
@@ -0,0 +1,272 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>#54 — ornament and frame specimen (zero images)</title>
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: Cinzel;
|
||||||
|
src: url("../../public/fonts/cinzel/cinzel-latin-600-normal.woff2") format("woff2");
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: Jost;
|
||||||
|
src: url("../../public/fonts/jost/jost-latin-400-normal.woff2") format("woff2");
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: Jost;
|
||||||
|
src: url("../../public/fonts/jost/jost-latin-600-normal.woff2") format("woff2");
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link rel="stylesheet" href="ornament.css" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--wg-bg);
|
||||||
|
color: var(--wg-text-soft);
|
||||||
|
font-family: var(--wg-font-body);
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
font-family: var(--wg-font-display);
|
||||||
|
color: var(--wg-text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
padding-block: 2.5rem;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.note {
|
||||||
|
color: var(--wg-text-muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: var(--wg-gold);
|
||||||
|
}
|
||||||
|
.swatchrow {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- SHELL: identical in both densities. Band + lintel + rail. -->
|
||||||
|
<div class="wg-band">
|
||||||
|
<div class="wg-rail" style="display: flex; gap: 1.5rem; align-items: center">
|
||||||
|
<span
|
||||||
|
style="
|
||||||
|
font-family: var(--wg-font-display);
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--wg-ledger-ink);
|
||||||
|
"
|
||||||
|
>Shadows Over Westgate</span
|
||||||
|
>
|
||||||
|
<span class="note">topbar — same band, same rail, every page</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wg-band wg-band--lintel wg-horizon" style="padding-block: 3rem">
|
||||||
|
<div class="wg-rail">
|
||||||
|
<h1 style="margin: 0 0 0.5rem; font-size: 2.6rem">The City and Its Ledger</h1>
|
||||||
|
<p class="note" style="margin: 0">
|
||||||
|
LINTEL + HORIZON — the threshold into the page. Zero images: the glow is a radial
|
||||||
|
gradient, the rule bleeds 32px of light downward.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main class="wg-rail">
|
||||||
|
<!-- ================= SHOWCASE DENSITY ================= -->
|
||||||
|
<section class="wg-showcase">
|
||||||
|
<h2 class="wg-head wg-head--earned wg-head--ruled">Showcase density</h2>
|
||||||
|
|
||||||
|
<p class="note">
|
||||||
|
Earned header: display face, uppercase, one colour token, and the single permitted
|
||||||
|
flourish — a gilt rule that starts at the text and fades to the rail.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="grid" style="margin-top: 1.5rem">
|
||||||
|
<div class="wg-panel wg-panel--velvet">
|
||||||
|
<h3 class="wg-head wg-head--earned" style="font-size: 0.95rem">Panel</h3>
|
||||||
|
<p>
|
||||||
|
The bounded surface. Its edge is a masked overlay — full strength at the top, gone
|
||||||
|
by the bottom. No hairline runs corner to corner.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="wg-panel wg-panel--plain">
|
||||||
|
<h3 class="wg-head" style="font-size: 0.95rem">Plain panel</h3>
|
||||||
|
<p>
|
||||||
|
The cheap panel. One tone down, softer edge, no sheen, no fade. This is what a
|
||||||
|
panel looks like when it has nothing to say.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="wg-panel wg-panel--velvet wg-legend">
|
||||||
|
<span class="wg-plaque">Plaque</span>
|
||||||
|
<p style="margin-top: 0.75rem">
|
||||||
|
A name plate, not a band — <code>inline-block</code> is the whole idea. As a
|
||||||
|
straddling legend it sits on its own panel edge.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="wg-head wg-head--earned wg-head--ruled" style="margin-top: 2.5rem">
|
||||||
|
The divider inventory
|
||||||
|
</h2>
|
||||||
|
<p class="note">Four. There is no fifth. Anything else is a habit, not a divider.</p>
|
||||||
|
|
||||||
|
<div class="wg-panel wg-panel--plain" style="margin-top: 1rem">
|
||||||
|
<p><strong>1. Ruling</strong> — row separation inside a ledger, and nowhere else.</p>
|
||||||
|
<div class="wg-ledger" style="margin: 0.75rem 0 1.5rem">
|
||||||
|
<div class="wg-ledger__row wg-ledger__row--marked">
|
||||||
|
<span style="color: var(--wg-ledger-ink)">A marked row</span>
|
||||||
|
<span class="note" style="margin-left: auto">14 replies · 2h</span>
|
||||||
|
</div>
|
||||||
|
<div class="wg-ledger__row">
|
||||||
|
<span>An ordinary row</span>
|
||||||
|
<span class="note" style="margin-left: auto">3 replies · 5h</span>
|
||||||
|
</div>
|
||||||
|
<div class="wg-ledger__row">
|
||||||
|
<span>Another ordinary row</span>
|
||||||
|
<span class="note" style="margin-left: auto">no replies · 1d</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><strong>2. Fading rule</strong> — between sections. It ends by fading, not by stopping.</p>
|
||||||
|
<hr class="wg-rule" style="margin: 0.75rem 0 1.5rem" />
|
||||||
|
|
||||||
|
<p><strong>3. Lintel</strong> — a threshold. One per band, at the top of the page shell.</p>
|
||||||
|
<div class="wg-band wg-band--lintel" style="margin: 0.75rem 0 1.5rem; padding: 1rem">
|
||||||
|
<span class="note">the band below a lintel</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><strong>4. Spine</strong> — a vertical fade marking a sequence: a thread of posts.</p>
|
||||||
|
<div class="wg-spine" style="margin-top: 0.75rem">
|
||||||
|
<p style="margin: 0">First reply in the thread.</p>
|
||||||
|
<p style="margin: 0.5rem 0 0">Second reply, same spine.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="wg-head wg-head--earned wg-head--ruled" style="margin-top: 2.5rem">
|
||||||
|
Icons, stamps, and the two metals
|
||||||
|
</h2>
|
||||||
|
<div class="wg-panel wg-panel--velvet" style="margin-top: 1rem">
|
||||||
|
<div class="swatchrow">
|
||||||
|
<span
|
||||||
|
class="wg-icon"
|
||||||
|
style="--wg-icon-src: url('../../public/game-icons/scroll-unfurled.svg')"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
class="wg-icon"
|
||||||
|
style="--wg-icon-src: url('../../public/game-icons/tavern-sign.svg')"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
class="wg-icon"
|
||||||
|
style="--wg-icon-src: url('../../public/game-icons/duality-mask.svg')"
|
||||||
|
></span>
|
||||||
|
<span class="note">pewter: structure and identity</span>
|
||||||
|
<span
|
||||||
|
class="wg-icon wg-icon--gilt"
|
||||||
|
style="--wg-icon-src: url('../../public/game-icons/wax-seal.svg')"
|
||||||
|
></span>
|
||||||
|
<span class="note">gilt: this matters, or can be touched</span>
|
||||||
|
<span class="wg-stamp">Closed</span>
|
||||||
|
<span class="note">the only red</span>
|
||||||
|
</div>
|
||||||
|
<p class="note" style="margin-bottom: 0">
|
||||||
|
Every icon is a mask over a token, never a coloured file — one asset serves every
|
||||||
|
state and forced-colors mode.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="wg-rule" />
|
||||||
|
|
||||||
|
<!-- ================= APP DENSITY ================= -->
|
||||||
|
<section class="wg-app">
|
||||||
|
<h2 class="wg-head wg-head--earned wg-head--ruled">App density — same rail, same shell</h2>
|
||||||
|
<p class="note">
|
||||||
|
Identical width, identical components. Sheen off, rows tighter, headers reduced, icons
|
||||||
|
one step smaller. Nothing here is a different component; it is the same set turned down.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 1rem; align-items: baseline; margin: 1rem 0">
|
||||||
|
<span class="wg-plaque">Roleplay</span>
|
||||||
|
<span class="note">the page title keeps its plaque; nothing else gets one</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wg-ledger">
|
||||||
|
<div class="wg-ledger__row wg-ledger__row--marked">
|
||||||
|
<span
|
||||||
|
class="wg-icon"
|
||||||
|
style="--wg-icon-src: url('../../public/game-icons/stabbed-note.svg')"
|
||||||
|
></span>
|
||||||
|
<span style="color: var(--wg-text)">Notices from the Night Market</span>
|
||||||
|
<span class="note" style="margin-left: auto">42 · 12m</span>
|
||||||
|
</div>
|
||||||
|
<div class="wg-ledger__row">
|
||||||
|
<span
|
||||||
|
class="wg-icon"
|
||||||
|
style="--wg-icon-src: url('../../public/game-icons/mute.svg')"
|
||||||
|
></span>
|
||||||
|
<span>Rumors heard near the docks</span>
|
||||||
|
<span class="note" style="margin-left: auto">9 · 1h</span>
|
||||||
|
</div>
|
||||||
|
<div class="wg-ledger__row">
|
||||||
|
<span
|
||||||
|
class="wg-icon"
|
||||||
|
style="--wg-icon-src: url('../../public/game-icons/sword-altar.svg')"
|
||||||
|
></span>
|
||||||
|
<span>Journal of a compromised clerk</span>
|
||||||
|
<span class="note" style="margin-left: auto">3 · 6h</span>
|
||||||
|
</div>
|
||||||
|
<div class="wg-ledger__row">
|
||||||
|
<span
|
||||||
|
class="wg-icon"
|
||||||
|
style="--wg-icon-src: url('../../public/game-icons/cowled.svg')"
|
||||||
|
></span>
|
||||||
|
<span>Faction standing, second quarter</span>
|
||||||
|
<span class="note" style="margin-left: auto">27 · 1d</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="wg-head" style="margin-top: 1.5rem">A reduced header (has not earned ornament)</h3>
|
||||||
|
<div class="wg-panel wg-panel--plain">
|
||||||
|
<p style="margin: 0">
|
||||||
|
Body face, no case switch, no rule, no background. The reduced header is the default;
|
||||||
|
ornament is the exception that has to be argued for.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="wg-rule" />
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 class="wg-head wg-head--earned">The standing test</h2>
|
||||||
|
<p class="note" style="max-width: 60ch">
|
||||||
|
This page contains no images of any kind — every frame, rule, plaque, glow, and icon
|
||||||
|
above is CSS or a mask over a token. If this page looks unfinished, the component set
|
||||||
|
is not done.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div class="wg-band wg-band--lintel" style="margin-top: 3rem; padding-block: 2rem">
|
||||||
|
<div class="wg-rail">
|
||||||
|
<span class="note">Footer — the same band and lintel that opened the page.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||