prototype(#54): variants and the settled ledger row

Adds the three category-list variants the set was judged against (roomy,
tabular, tabular+gold) and the fixes that came out of looking at them in a
browser:

- square everywhere, with enough specificity to beat the theme's own
  `border-radius: 8px !important`; only the status dot stays round
- a drop shadow under every bounded surface, so a square edge reads as an
  edge and not a cut
- Cinzel always uppercase
- the Well component: the three right-hand readings sit in recesses on a
  shared grid, inset on all four sides
- the clamped teaser clamps by line count, not by NodeBB's hard max-height,
  which was cutting the glyphs
This commit is contained in:
2026-07-31 18:00:47 +02:00
parent 1a479023d5
commit 7a29402d93
21 changed files with 310 additions and 12 deletions
+2 -2
View File
@@ -55,7 +55,7 @@
.wg-panel { .wg-panel {
position: relative; position: relative;
background: var(--wg-panel); background: var(--wg-panel);
border-radius: 8px; border-radius: 0;
padding: var(--wg-pad, 1rem); padding: var(--wg-pad, 1rem);
} }
.wg-panel::before { .wg-panel::before {
@@ -141,7 +141,7 @@
====================================================================== */ ====================================================================== */
.wg-ledger { .wg-ledger {
background: var(--wg-panel-2); background: var(--wg-panel-2);
border-radius: 8px; border-radius: 0;
overflow: hidden; overflow: hidden;
} }
.wg-ledger__row { .wg-ledger__row {
+68 -10
View File
@@ -4,24 +4,73 @@
Every rule below is one of the named components doing its job on real Every rule below is one of the named components doing its job on real
NodeBB markup. Nothing new is invented here. */ NodeBB markup. Nothing new is invented here. */
/* ---- PANEL: edges fade. No hairline runs corner to corner. ---- */ /* ---- SQUARE. One shape, every surface. A cut or rounded corner is a second
shape language and the shell only gets one. ---- */
/* specificity has to beat the theme's own `border-radius: 8px !important` */
html body [class],
html body [class]::before,
html body [class]::after,
html body [class] [class],
html body [class] [class][class],
html body blockquote,
html body pre,
html body code,
html body img,
html body input,
html body textarea,
html body button {
border-radius: 0 !important;
}
/* Signets stay square like everything else — the theme's own signet plate is
the settled treatment and it is square. Only the status dot is round,
because it is a dot. */
html body [class] [class].status,
html body .status {
border-radius: 50% !important;
}
/* ---- PANEL: edges fade. No hairline runs corner to corner, but the edge is
present enough to hold the panel off the page. ---- */
li[component="categories/category"], li[component="categories/category"],
li[component="category/topic"], li[component="category/topic"],
.westgate-surface, .westgate-surface,
.card { .card,
ul.categories-list,
ul.westgate-categories-list,
[component="post/content"],
.post-container {
border-color: transparent !important; border-color: transparent !important;
position: relative; position: relative;
} }
li[component="categories/category"]::after, ul.categories-list::after,
.westgate-surface::after { ul.westgate-categories-list::after,
.westgate-surface::after,
.post-container::after,
body.template-category .category-header::after {
content: ""; content: "";
position: absolute; position: absolute;
inset: 0; inset: 0;
border-radius: inherit; border: 1px solid rgba(var(--wg-pewter-rgb), 0.34);
border: 1px solid var(--wg-border); -webkit-mask-image: linear-gradient(180deg, #000, rgba(0, 0, 0, 0.16));
-webkit-mask-image: linear-gradient(180deg, #000, rgba(0, 0, 0, 0.12)); mask-image: linear-gradient(180deg, #000, rgba(0, 0, 0, 0.16));
mask-image: linear-gradient(180deg, #000, rgba(0, 0, 0, 0.12));
pointer-events: none; pointer-events: none;
z-index: 2;
}
/* ---- A square edge with no falloff reads as a cut, not an edge. Every
bounded surface sits on a wide, offset-down black shadow: no lift, no
stacking claim, just the corner catching the room (Candlelight). ---- */
html body ul.categories-list,
html body ul.westgate-categories-list,
html body .westgate-surface,
html body .post-container,
html body .topic-list-header,
html body [data-widget-area] .card,
html body.template-category .category-header {
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.02),
0 10px 26px rgba(0, 0, 0, 0.34),
0 2px 4px rgba(0, 0, 0, 0.28) !important;
} }
/* ---- LEDGER: rows are ruled, not boxed. The category list is a ledger: /* ---- LEDGER: rows are ruled, not boxed. The category list is a ledger:
@@ -30,8 +79,6 @@ ul.categories-list,
ul.westgate-categories-list { ul.westgate-categories-list {
gap: 0 !important; gap: 0 !important;
background: var(--wg-panel-2); background: var(--wg-panel-2);
border-radius: 8px;
overflow: hidden;
} }
ul.categories-list li[component="categories/category"], ul.categories-list li[component="categories/category"],
ul.westgate-categories-list li[component="categories/category"] { ul.westgate-categories-list li[component="categories/category"] {
@@ -71,6 +118,17 @@ li[component="categories/category"] h2.title::after,
background: linear-gradient(90deg, rgba(var(--wg-gold-rgb), 0.34), transparent); background: linear-gradient(90deg, rgba(var(--wg-gold-rgb), 0.34), transparent);
} }
/* ---- CASE: Cinzel is always uppercase. One rule, no exceptions, because
its lowercase is a small-caps design and reads as a weaker version of the
same thing rather than a second voice. ---- */
html body [class] h1,
html body [class] h2,
html body [class] h3,
html body .topic-title,
html body .breadcrumb {
text-transform: uppercase !important;
}
/* ---- PLAQUE: the page title becomes a name plate, not a band. ---- */ /* ---- PLAQUE: the page title becomes a name plate, not a band. ---- */
body.template-category .category-header .category-title, body.template-category .category-header .category-title,
body.template-topic .topic-title h1 { body.template-topic .topic-title h1 {
+1
View File
@@ -14,6 +14,7 @@ const ornament = read('real-surface.css');
const VARIANTS = { before: paletteB, after: paletteB + ornament }; const VARIANTS = { before: paletteB, after: paletteB + ornament };
const PAGES = [ const PAGES = [
['categories', '/categories'], ['categories', '/categories'],
['category', '/category/8'],
['topic', '/topic/2'], ['topic', '/topic/2'],
]; ];
Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 555 KiB

After

Width:  |  Height:  |  Size: 559 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 633 KiB

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 966 KiB

After

Width:  |  Height:  |  Size: 962 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 961 KiB

After

Width:  |  Height:  |  Size: 969 KiB

+30
View File
@@ -0,0 +1,30 @@
/* Variant: gilt structures, pewter only speaks.
Two Metals inverted from #53's assignment — the metals keep separate jobs,
but the visible edge work goes back to gold and pewter is demoted to
metadata text only. Answers "there is too much silver now". */
:root {
--wg-border: rgba(var(--wg-gold-rgb), 0.2);
--wg-border-soft: rgba(var(--wg-gold-rgb), 0.11);
--wg-ledger-ruling: rgba(var(--wg-gold-rgb), 0.08);
--wg-ledger-rule-strong: rgba(var(--wg-gold-rgb), 0.2);
--wg-ledger-border: rgba(var(--wg-gold-rgb), 0.24);
}
/* panel edges, ledger edge, icon ring: gilt */
html body ul.categories-list::after,
html body ul.westgate-categories-list::after,
html body .westgate-surface::after,
html body .post-container::after,
html body.template-category .category-header::after {
border-color: rgba(var(--wg-gold-rgb), 0.28) !important;
}
html body 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-gold-rgb), 0.28) !important;
}
/* category names keep the ledger ink, one step brighter than body */
html body li[component="categories/category"] h2.title,
html body li[component="categories/category"] h2.title a {
color: var(--wg-ledger-ink) !important;
}
+30
View File
@@ -0,0 +1,30 @@
/* Variant: rows breathe. The category list goes back to separate panels with
air between them, instead of one ruled ledger. Same components, same square
shape, same fading edge — only the ledger reading is dropped. */
html body ul.categories-list,
html body ul.westgate-categories-list {
gap: var(--wg-space-sm, 0.75rem) !important;
background: none !important;
}
html body ul.categories-list::after,
html body ul.westgate-categories-list::after {
content: none;
}
html body ul.categories-list li[component="categories/category"],
html body ul.westgate-categories-list li[component="categories/category"] {
background: var(--wg-panel-2) !important;
background-image: none !important;
padding: var(--wg-space-md, 1rem) !important;
}
/* each row gets its own fading edge, since it is a panel again */
html body ul.categories-list li[component="categories/category"]::after,
html body ul.westgate-categories-list li[component="categories/category"]::after {
content: "";
position: absolute;
inset: 0;
border: 1px solid rgba(var(--wg-pewter-rgb), 0.34);
-webkit-mask-image: linear-gradient(180deg, #000, rgba(0, 0, 0, 0.16));
mask-image: linear-gradient(180deg, #000, rgba(0, 0, 0, 0.16));
pointer-events: none;
z-index: 2;
}
+179
View File
@@ -0,0 +1,179 @@
/* Variant: the ledger row as a line in a record, not three blocks in a row.
The data columns move into WELLS; the left side stays prose. */
/* ---- WELL — a recess, the inverse of Panel. Panel is laid on the surface
and casts down; a well is cut into it and shadows inward. Three of them,
one per reading, bleeding to the row's right edge. The gap between two
wells is their divider: a well never carries a rule inside it. ---- */
html body li[component="categories/category"] > div.col-lg-5 {
background: none !important;
box-shadow: none !important;
align-items: stretch !important;
padding: 0 !important;
gap: 0.4rem !important;
/* the wells sit inside the row's margin on every side — a well cut flush to
the right edge reads as the row running off the page */
margin: 0 !important;
}
/* the stat pair: two equal columns of well, not two little cards */
html body li[component="categories/category"] .meta.stats,
html body li[component="categories/category"] .stats {
display: grid !important;
/* the counts need only their own width; the room goes to the teaser */
grid-template-columns: 6rem 6rem !important;
justify-content: end !important;
gap: 0.4rem !important;
padding: 0 !important;
border: 0 !important;
background: none !important;
box-shadow: none !important;
}
/* the well itself */
html body li[component="categories/category"] .stats .card,
html body li[component="categories/category"] .stats .card-header,
html body li[component="categories/category"] .teaser {
/* the basin sits below the row it is cut into, but not as dead as the
page floor: between panel-2 and bg-deep */
background: color-mix(in srgb, var(--wg-panel-2) 45%, var(--wg-bg-deep)) !important;
background-image: none !important;
border: 0 !important;
box-shadow:
inset 0 1px 3px rgba(0, 0, 0, 0.55),
inset 0 0 0 1px rgba(var(--wg-gold-rgb), 0.1) !important;
padding: 0.5rem 0.85rem !important;
}
/* the teaser is prose, so it gets the room and a proper margin of its own */
html body li[component="categories/category"] .teaser {
flex: 1 1 auto !important;
/* the basin's inset is even on all four sides — a wider left/right inset
reads as the text not filling the well */
padding: 0.55rem !important;
}
/* the teaser's inner blocks are flex items once the well is a flex box, so
they shrink to their text and stop filling. Make them fill it. */
html body li[component="categories/category"] .lastpost,
html body li[component="categories/category"] .westgate-lastpost {
flex: 1 1 auto !important;
width: 100% !important;
min-width: 0 !important;
height: auto !important;
}
html body li[component="categories/category"] [component="category/posts"] {
padding-left: 0 !important;
width: 100% !important;
min-width: 0 !important;
/* the byline and its line of prose are one block, not two rows */
gap: 0.1rem !important;
height: auto !important;
line-height: 1.35 !important;
}
html body li[component="categories/category"] .teaser .text-nowrap {
line-height: 1.2 !important;
/* the signet's ring is drawn outside its 18px box, and this row is
`text-truncate` (overflow: hidden), so at x=0 the left of the ring is cut.
Give it the ring's width back. */
padding-left: 2px !important;
}
html body li[component="categories/category"] .stats .card {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
}
/* the number carries; the label is engraved and small */
html body li[component="categories/category"] .stats .card > span:first-child {
font-family: var(--wg-font-display) !important;
color: var(--wg-text) !important;
}
html body li[component="categories/category"] .stats .card > span + span,
html body li[component="categories/category"] .stats small {
font-family: var(--wg-font-display) !important;
font-size: 0.78rem !important;
letter-spacing: 0.12em !important;
text-transform: uppercase !important;
color: var(--wg-text-muted) !important;
}
/* the teaser well reads as text, so it is set left and loses NodeBB's
inline pure-white rule — a ledger has one ruling weight, not two */
html body li[component="categories/category"] .teaser {
display: flex !important;
align-items: center !important;
}
html body li[component="categories/category"] .teaser *,
html body li[component="categories/category"] .lastpost,
html body li[component="categories/category"] .westgate-lastpost {
text-align: left !important;
border-left: 0 !important;
padding-left: 0 !important;
}
/* the byline row must stretch, or it shrinks to its own text and the
`text-truncate` on it starts clipping the signet */
html body li[component="categories/category"] [component="category/posts"],
html body li[component="categories/category"] .lastpost,
html body li[component="categories/category"] .westgate-lastpost {
align-items: stretch !important;
justify-content: center !important;
}
html body li[component="categories/category"] .teaser .text-nowrap,
html body li[component="categories/category"] .teaser .post-content {
width: 100% !important;
max-width: 100% !important;
}
html body li[component="categories/category"] .teaser p,
html body li[component="categories/category"] [component="category/posts"] p {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin: 0 !important;
color: var(--wg-text-muted) !important;
}
/* the left side is prose and stops at its own measure; the wells take the
room that was standing empty between the two */
html body li[component="categories/category"] > div.col-lg-7 {
flex: 0 0 46% !important;
max-width: 46% !important;
}
html body li[component="categories/category"] > div.col-lg-5 {
flex: 1 1 auto !important;
max-width: none !important;
}
/* one baseline for the whole row, so rows scan as lines rather than pages.
The columns stretch: a centred well cannot grow, so a two-line teaser
overflows it top and bottom and gets clipped. */
html body li[component="categories/category"] {
align-items: stretch !important;
}
html body li[component="categories/category"] .teaser,
html body li[component="categories/category"] .lastpost,
html body li[component="categories/category"] .westgate-lastpost,
html body li[component="categories/category"] [component="category/posts"] {
height: auto !important;
min-height: 0 !important;
overflow: visible !important;
}
/* the clamp keeps its own overflow — it is what puts the ellipsis on. But its
line box has to be taller than the glyphs, or `overflow: hidden` shaves the
ascenders off the first line and the descenders off the last. */
html body li[component="categories/category"] .post-content {
height: auto !important;
min-height: 0 !important;
line-height: 1.5 !important;
/* NodeBB's `line-clamp-sm-2` clamps by a hard `max-height` sized to its own
line-height. Ours is taller, so that max-height cuts the glyphs. Drop it
and clamp by line count instead. */
max-height: none !important;
display: -webkit-box !important;
-webkit-line-clamp: 2 !important;
-webkit-box-orient: vertical !important;
overflow: hidden !important;
}
html body li[component="categories/category"] h2.title {
margin-bottom: 0.15rem !important;
}