Role-based token contract #55

Closed
opened 2026-07-28 08:10:38 +00:00 by archvillainette · 2 comments
Owner

Part of #47

Question

What is the token contract, expressed as roles rather than colour names?

DESIGN.md frontmatter currently lists 26 named colours — tarnished-gold, bruised-plum, crypt-black, heart-wine — and _tokens.scss is 155 lines. Names like these describe pigment, not job, so every consumer has to know which pigment means "border" this week. That is exactly what makes a palette change expensive, and this map exists to make one.

Resolve:

  • The role vocabulary: surface levels, border levels, text levels, accent, state, focus. How many of each actually exist, established from real usage across the 6118 lines in scss/westgate/, not invented.
  • How poetic names and roles coexist. Both have value — the names carry the setting, the roles carry the code — so decide whether names alias roles, roles alias names, or names live only in prose.
  • What the alpha ladders become. Borders are currently documented at 8% / 14% / 18-22% gold; if the second metal or light surface lands, that ladder needs re-deriving rather than reusing.
  • Which tokens are consumed outside this repo — the pages, wiki, and support plugins all style against them — and therefore constitute a public contract that cannot be renamed casually.
  • What happens to the vote-state families (heart-*, sorrow-*), which are pigment-named state colours and the clearest case of the problem.
  • Whether _tokens.scss stays one file.

Output is the token contract as a list of roles with their values from the winning palette, plus the migration note for external consumers.

Part of #47 ## Question What is the token contract, expressed as roles rather than colour names? `DESIGN.md` frontmatter currently lists 26 named colours — `tarnished-gold`, `bruised-plum`, `crypt-black`, `heart-wine` — and `_tokens.scss` is 155 lines. Names like these describe pigment, not job, so every consumer has to know which pigment means "border" this week. That is exactly what makes a palette change expensive, and this map exists to make one. Resolve: - The role vocabulary: surface levels, border levels, text levels, accent, state, focus. How many of each actually exist, established from real usage across the 6118 lines in `scss/westgate/`, not invented. - How poetic names and roles coexist. Both have value — the names carry the setting, the roles carry the code — so decide whether names alias roles, roles alias names, or names live only in prose. - What the alpha ladders become. Borders are currently documented at 8% / 14% / 18-22% gold; if the second metal or light surface lands, that ladder needs re-deriving rather than reusing. - Which tokens are consumed outside this repo — the pages, wiki, and support plugins all style against them — and therefore constitute a public contract that cannot be renamed casually. - What happens to the vote-state families (`heart-*`, `sorrow-*`), which are pigment-named state colours and the clearest case of the problem. - Whether `_tokens.scss` stays one file. Output is the token contract as a list of roles with their values from the winning palette, plus the migration note for external consumers.
archvillainette added the wayfinder:grilling label 2026-07-28 08:10:38 +00:00
archvillainette added a new dependency 2026-07-28 08:10:54 +00:00
Author
Owner

Handed over from #53 (resolved)

The palette this contract has to express is decided — candidate B, Two Metals. Full role
assignment in the #53 resolution.

Three things this ticket now owns:

  1. There are two palette layers, not one. scss/overrides.scss holds Sass variables
    ($wg-ink, $gray-*, $black, $dropdown-bg) that must be real colours, not
    var(), because Sass passes them to rgba() and Bootstrap. They do not follow a CSS
    custom-property swap: in the prototype shots the topbar kept the old cold ground while the
    page below it moved. The contract has to say how the two layers stay in agreement.
  2. The colours are mostly literal today — 233 rgba(194, 163, 90, …) uses alone. A
    role-based contract is unusable until they are tokenised. Branch
    prototype/53-palette-candidates has a working mechanical pass to crib from.
  3. The three panel tones (graduated out of the map's fog by #53). Pewter, not a third tone,
    now carries structure — so the live question is which of velvet-panel / -deep /
    -raised still earns a slot. That is this ticket's to answer.
## Handed over from #53 (resolved) The palette this contract has to express is decided — candidate B, Two Metals. Full role assignment in the #53 resolution. Three things this ticket now owns: 1. **There are two palette layers, not one.** `scss/overrides.scss` holds Sass variables (`$wg-ink`, `$gray-*`, `$black`, `$dropdown-bg`) that **must** be real colours, not `var()`, because Sass passes them to `rgba()` and Bootstrap. They do not follow a CSS custom-property swap: in the prototype shots the topbar kept the old cold ground while the page below it moved. The contract has to say how the two layers stay in agreement. 2. **The colours are mostly literal today** — 233 `rgba(194, 163, 90, …)` uses alone. A role-based contract is unusable until they are tokenised. Branch `prototype/53-palette-candidates` has a working mechanical pass to crib from. 3. **The three panel tones** (graduated out of the map's fog by #53). Pewter, not a third tone, now carries structure — so the live question is which of `velvet-panel` / `-deep` / `-raised` still earns a slot. That is this ticket's to answer.
archvillainette self-assigned this 2026-07-31 22:20:49 +00:00
Author
Owner

Resolution

The token contract is ~30 roles, down from 50 declared tokens plus 26 frontmatter pigments.
Derived from real usage across scss/westgate/ (6118 lines) and the sibling plugin checkouts,
not invented.

The contract

Family Roles Values (#53 candidate B)
Surface page, page-deep, panel, panel-quiet, accent #150E13, #0C0810, #21161C, #180F16, accent pending
Accent accent, accent-lift, accent-glow / -glow-raised Bruised Plum lineage, pending (see below)
Text text, text-soft, text-muted #E6E0D6, #B9B2A6, #9EA7B3 pewter
Structure rule, rule-soft, rule-faint, edge-gold pewter α .20 / .11 / .075; Old Gilt #A8893F opaque
Value metal gold, gold-lit, ink #C2A35A, #E0C878, #D8C28A
Focus focus-ring #C2A35A, outline-offset: 2px (#50 finding b)
State status-online/away/dnd/offline, vote-on, danger, stamp statuses unchanged; #d47a87; #8E3438; #A84A4E
Non-colour 6 spacing, ledger-line, page-width, 4 fonts unchanged; page-width is #56's, not this contract's

The eight rules

  1. Roles are the contract; poetic names live in DESIGN.md prose only. One table maps role →
    value → name (--wg-text-muted #9EA7B3 pewter). No pigment name appears in code, in
    frontmatter, or in a consumer. The frontmatter colors: block of 26 pigments goes away —
    it duplicated token values by hand, so a re-tune had to edit both and they could drift
    silently. Name-aliasing tokens were rejected: a second public surface to keep AA-checked,
    and renames cost double.
  2. Velvet and Ledger collapse into one panel surface. They differ by opacity and shadow,
    not by tone, and #54 already split them as components (Velvet panel vs Ledger, sheen on or
    off). Material belongs to the component, not to a second surface token. panel-quiet
    survives for recessed things — card caps, inputs, #54's Well. panel-3 and plum-soft are
    dropped, not renamed; #4A2A38 stays a documented accent value with #50's rule-2 limit
    (large text and non-text only) but earns no role until a component asks.
  3. Three pewter structure steps, and that is the whole ladder. All 85 gold border uses snap
    to the nearest of .20 / .11 / .075; no bespoke alphas. Gold as an edge means value,
    not structure, so edge-gold is opaque — because gold and pewter are luminance twins
    (1.006:1, #50), a pewter rule and a gold rule at the same alpha are the same lightness. The
    ladder may never mean "more important".
  4. Plum is identity, carried by accent and glow, never by the ground. Three accent roles:
    accent (opaque surface — icon plates, plaques, bands), accent-lift (hover/active), and
    accent-glow in two steps (rest/raised) for the inner bloom inside panels and behind
    ornament. The glow role absorbs #602044, an undeclared third plum found inside
    --wg-velvet-shadow.
  5. Syntax highlighting leaves the palette. moss-string, dry-apricot and deletion-red
    were never UI states — they are highlight.js colours in _posts.scss. They become scoped
    --wg-code-* vars next to that block. The five heart-*/sorrow-* names die: only
    #d47a87 was ever used (the upvote arrow) and it becomes the single vote-on role.
    Downvote keeps plain gold and gets no colour of its own — a downvote is not a colour event.
    stamp #A84A4E survives apart from danger because it is ornament (the wax seal, #54's
    Stamp), not an alert.
  6. One Sass map is the single source of truth. _palette.scss holds role → value;
    _tokens.scss emits :root { --wg-#{$role}: … } with an @each; overrides.scss reads
    the same map. This closes #53's two-layer seam — the Sass layer cannot follow a var()
    swap, which is why the prototype's topbar stayed cold while the page moved. The Sass side is
    derivation only (rgba(), mix(), Bootstrap plumbing); it may never invent a colour.
    Bootstrap's $gray-100…900 stops being its own ramp and maps onto roles, as do $primary
    and $danger; $blue/$green/$cyan get roles or get pinned, because today Bootstrap can
    paint a component in a colour nobody chose. NodeBB compiles the SCSS, so this needs no build
    step in this repo.
  7. A contract test bans colour literals outside _palette.scss (allowing #fff and
    transparent). One more node tests/<name>.test.js file. This is the only thing that stops
    literal #234 appearing next month — the theme currently has 233 rgba(194,163,90,…) uses,
    44 rgba(216,194,138,…), 40 rgba(255,244,221,…) and 18 literal #fff4dd.
  8. Rename hard, no alias layer, and a marked private tier. The public tier is a documented
    subset — surfaces, text, accent, structure, gold, state, spacing, fonts. Ornament internals
    (etched-*, the shadow composites, control-bg) are private, carry a distinct prefix so
    the boundary is visible in code, and #54's components may change them without it counting as
    a break.

Which tokens are public, established by grep

  • sow-nodebb-plugin-pages consumes 18 distinct tokens (--wg-border, -text-muted,
    -space-md, -gold, -font-display, -velvet-panel, -velvet-shadow, -font-text, …).
    Two of them — velvet-panel and velvet-shadow — this ticket abolishes.
  • sow-nodebb-plugin-wiki and sow-nodebb-plugin-support consume none directly. The
    wiki styles against --wiki-*, which this theme binds to --wg-* in _wiki-prose.scss.
    That indirection is the right shape and stays — do not flatten it.
  • Migration is therefore one repo and 18 names. No shim: the theme is pinned by exact commit in
    sow-nodebb's plugins.lock, so old theme and old pages keep working together until both are
    re-pinned. An alias layer would be permanent debt bought to protect one sibling checkout.
    The migration table (old name → new role → who changes) is filed against pages when the
    palette ships
    , not now.

Does _tokens.scss stay one file

No. It splits: _palette.scss (the map — values, the one place a re-tune edits) and
_tokens.scss (the emit, plus spacing, type scale and @font-face). The split is what makes
rule 6 possible; it is not filing preference.

What this ticket does not close

The accent value is pending. During this grilling the human reversed a standing law of the
map: Bruised Plum is preferred over Plum Noir, as the accent pushed harder than today — not
as the ground. #53's four ground surfaces stay on the Plum Noir lineage. The new value needs a
contrast run (#2a1222 is darker than Plum Noir, which #53 already measured at the Dust 4.90
ceiling) and a browser look. Raised as its own ticket rather than settled quietly here.

Riding along on that ticket: #d47a87 vote-on has never been contrast-checked against the
new grounds, and it is the one saturated pink on a page that outlawed lavender.

## Resolution The token contract is **~30 roles**, down from 50 declared tokens plus 26 frontmatter pigments. Derived from real usage across `scss/westgate/` (6118 lines) and the sibling plugin checkouts, not invented. ### The contract | Family | Roles | Values (#53 candidate B) | |---|---|---| | Surface | `page`, `page-deep`, `panel`, `panel-quiet`, `accent` | `#150E13`, `#0C0810`, `#21161C`, `#180F16`, accent **pending** | | Accent | `accent`, `accent-lift`, `accent-glow` / `-glow-raised` | Bruised Plum lineage, **pending** (see below) | | Text | `text`, `text-soft`, `text-muted` | `#E6E0D6`, `#B9B2A6`, `#9EA7B3` pewter | | Structure | `rule`, `rule-soft`, `rule-faint`, `edge-gold` | pewter α .20 / .11 / .075; Old Gilt `#A8893F` opaque | | Value metal | `gold`, `gold-lit`, `ink` | `#C2A35A`, `#E0C878`, `#D8C28A` | | Focus | `focus-ring` | `#C2A35A`, `outline-offset: 2px` (#50 finding b) | | State | `status-online/away/dnd/offline`, `vote-on`, `danger`, `stamp` | statuses unchanged; `#d47a87`; `#8E3438`; `#A84A4E` | | Non-colour | 6 spacing, `ledger-line`, `page-width`, 4 fonts | unchanged; `page-width` is #56's, not this contract's | ### The eight rules 1. **Roles are the contract; poetic names live in DESIGN.md prose only.** One table maps role → value → name (`--wg-text-muted` `#9EA7B3` *pewter*). No pigment name appears in code, in frontmatter, or in a consumer. The frontmatter `colors:` block of 26 pigments goes away — it duplicated token values by hand, so a re-tune had to edit both and they could drift silently. Name-aliasing tokens were rejected: a second public surface to keep AA-checked, and renames cost double. 2. **Velvet and Ledger collapse into one `panel` surface.** They differ by opacity and shadow, not by tone, and #54 already split them as *components* (Velvet panel vs Ledger, sheen on or off). Material belongs to the component, not to a second surface token. `panel-quiet` survives for recessed things — card caps, inputs, #54's Well. `panel-3` and `plum-soft` are dropped, not renamed; `#4A2A38` stays a documented accent value with #50's rule-2 limit (large text and non-text only) but earns no role until a component asks. 3. **Three pewter structure steps, and that is the whole ladder.** All 85 gold border uses snap to the nearest of `.20` / `.11` / `.075`; no bespoke alphas. Gold as an edge means *value*, not structure, so `edge-gold` is opaque — because gold and pewter are luminance twins (1.006:1, #50), a pewter rule and a gold rule at the same alpha are the same lightness. The ladder may never mean "more important". 4. **Plum is identity, carried by accent and glow, never by the ground.** Three accent roles: `accent` (opaque surface — icon plates, plaques, bands), `accent-lift` (hover/active), and `accent-glow` in two steps (rest/raised) for the inner bloom inside panels and behind ornament. The glow role absorbs `#602044`, an undeclared third plum found inside `--wg-velvet-shadow`. 5. **Syntax highlighting leaves the palette.** `moss-string`, `dry-apricot` and `deletion-red` were never UI states — they are highlight.js colours in `_posts.scss`. They become scoped `--wg-code-*` vars next to that block. The five `heart-*`/`sorrow-*` names die: only `#d47a87` was ever used (the upvote arrow) and it becomes the single `vote-on` role. Downvote keeps plain gold and gets no colour of its own — a downvote is not a colour event. `stamp` `#A84A4E` survives apart from `danger` because it is ornament (the wax seal, #54's Stamp), not an alert. 6. **One Sass map is the single source of truth.** `_palette.scss` holds role → value; `_tokens.scss` emits `:root { --wg-#{$role}: … }` with an `@each`; `overrides.scss` reads the same map. This closes #53's two-layer seam — the Sass layer cannot follow a `var()` swap, which is why the prototype's topbar stayed cold while the page moved. The Sass side is **derivation only** (`rgba()`, `mix()`, Bootstrap plumbing); it may never invent a colour. Bootstrap's `$gray-100…900` stops being its own ramp and maps onto roles, as do `$primary` and `$danger`; `$blue`/`$green`/`$cyan` get roles or get pinned, because today Bootstrap can paint a component in a colour nobody chose. NodeBB compiles the SCSS, so this needs no build step in this repo. 7. **A contract test bans colour literals outside `_palette.scss`** (allowing `#fff` and `transparent`). One more `node tests/<name>.test.js` file. This is the only thing that stops literal #234 appearing next month — the theme currently has 233 `rgba(194,163,90,…)` uses, 44 `rgba(216,194,138,…)`, 40 `rgba(255,244,221,…)` and 18 literal `#fff4dd`. 8. **Rename hard, no alias layer, and a marked private tier.** The public tier is a documented subset — surfaces, text, accent, structure, gold, state, spacing, fonts. Ornament internals (`etched-*`, the shadow composites, `control-bg`) are **private**, carry a distinct prefix so the boundary is visible in code, and #54's components may change them without it counting as a break. ### Which tokens are public, established by grep - **sow-nodebb-plugin-pages** consumes **18** distinct tokens (`--wg-border`, `-text-muted`, `-space-md`, `-gold`, `-font-display`, `-velvet-panel`, `-velvet-shadow`, `-font-text`, …). Two of them — `velvet-panel` and `velvet-shadow` — this ticket abolishes. - **sow-nodebb-plugin-wiki** and **sow-nodebb-plugin-support** consume **none** directly. The wiki styles against `--wiki-*`, which this theme binds to `--wg-*` in `_wiki-prose.scss`. That indirection is the right shape and stays — do not flatten it. - Migration is therefore one repo and 18 names. No shim: the theme is pinned by exact commit in sow-nodebb's `plugins.lock`, so old theme and old pages keep working together until both are re-pinned. An alias layer would be permanent debt bought to protect one sibling checkout. The migration table (old name → new role → who changes) is filed against pages **when the palette ships**, not now. ### Does `_tokens.scss` stay one file No. It splits: `_palette.scss` (the map — values, the one place a re-tune edits) and `_tokens.scss` (the emit, plus spacing, type scale and `@font-face`). The split is what makes rule 6 possible; it is not filing preference. ### What this ticket does not close **The accent value is pending.** During this grilling the human reversed a standing law of the map: Bruised Plum is preferred over Plum Noir, *as the accent pushed harder than today* — not as the ground. #53's four ground surfaces stay on the Plum Noir lineage. The new value needs a contrast run (`#2a1222` is darker than Plum Noir, which #53 already measured at the Dust 4.90 ceiling) and a browser look. Raised as its own ticket rather than settled quietly here. Riding along on that ticket: `#d47a87` `vote-on` has never been contrast-checked against the new grounds, and it is the one saturated pink on a page that outlawed lavender.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Depends on
#53 Palette candidates on a dense page
ShadowsOverWestgate/sow-nodebb-theme
Reference: ShadowsOverWestgate/sow-nodebb-theme#55