Co-Branded Pages: give Fastigium a change of kind, not a bigger number

Once every member from Ascensus up could hold several pages, the Funnel
Factory's differentiator became a count — the same quantity-not-kind flaw the
upper tiers had. L6 now unlocks identity instead: a member's own name,
monogram and accent colour on every page they build. Below Fastigium you have
a page on the team site; at Fastigium it becomes yours.

Three deliberate limits.

NO IMAGE UPLOADS. A logo file uploaded by a member and served from
rmcircle.team is an abuse surface — whatever they upload lives on our domain.
The mark is a monogram generated from their brand name instead: real identity,
nothing to moderate. Same reasoning that keeps Traffic Desk creative
team-only.

ACCENTS ARE A CURATED SET, not a colour picker. Every option is checked
against the dark ground, so nobody can pick something that makes their own
page unreadable and then wonder why it converts badly.

THE DISCLAIMERS ARE NOT THEIRS TO BRAND. Branding replaces the header
identity only; the footer — independent team resource, no income guaranteed,
cryptocurrency risk — is rendered by us and cannot be removed or restyled.
Co-branding, not white-labelling. Verified in the renderer.

Branding is resolved against the LIVE contract level at render time, so a
position that drops below Fastigium reverts to team identity on its own
rather than keeping something it no longer holds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-29 06:14:28 -05:00
parent c8ae4d55b0
commit a57af32f4a
7 changed files with 382 additions and 4 deletions
+111
View File
@@ -0,0 +1,111 @@
// Circle Suite — Co-branded pages (Fastigium, L6).
//
// Why this is the level-6 tool rather than "you may now have more pages":
// every member from Ascensus up can hold several pages, so a bigger number was
// never a real unlock — it is the same quantity-not-kind mistake the upper
// tiers used to make. This is a change of KIND. Below L6 a member has a page on
// the team site. At L6 it becomes their own branded presence.
//
// Three deliberate limits:
//
// 1. NO IMAGE UPLOADS. A logo file uploaded by a member and served from
// rmcircle.team is an abuse surface — anything they upload lives on our
// domain. Instead the mark is a monogram generated from their own brand
// name, which gives real identity with nothing to moderate. Same reasoning
// that keeps Traffic Desk creative team-only.
// 2. ACCENT COLOURS ARE A CURATED SET, not a free picker. Every option is
// checked against the dark ground, so nobody can pick a colour that makes
// their own page unreadable and then wonder why it converts badly.
// 3. THE DISCLAIMERS ARE NOT THEIRS TO BRAND. Branding replaces the header
// identity only. The footer — independent team resource, no income
// guaranteed, cryptocurrency risk — is rendered by us and cannot be
// removed, renamed or restyled. This is co-branding, not white-labelling.
'use strict';
const fs = require('fs');
const path = require('path');
let DATA_DIR = null;
function init(opts) {
DATA_DIR = opts.dataDir;
try { fs.mkdirSync(dir(), { recursive: true }); } catch (e) {}
}
function dir() { return path.join(DATA_DIR, 'brand'); }
function file(id) { return path.join(dir(), String(Number(id)) + '.json'); }
const MIN_LEVEL = 6;
// Each accent is checked to hold contrast on the #071421 ground. `soft` is the
// same hue at low alpha for panel washes.
const ACCENTS = {
gold: { label: 'Gold', hex: '#f3be43', soft: 'rgba(243,190,67,.12)' },
teal: { label: 'Teal', hex: '#4ed6cb', soft: 'rgba(78,214,203,.12)' },
violet: { label: 'Violet', hex: '#a98bff', soft: 'rgba(169,139,255,.14)' },
coral: { label: 'Coral', hex: '#ff8a6b', soft: 'rgba(255,138,107,.13)' },
lime: { label: 'Lime', hex: '#9ede4f', soft: 'rgba(158,222,79,.12)' },
sky: { label: 'Sky', hex: '#5cb8ff', soft: 'rgba(92,184,255,.13)' },
rose: { label: 'Rose', hex: '#ff7fa8', soft: 'rgba(255,127,168,.13)' },
amber: { label: 'Amber', hex: '#ffb020', soft: 'rgba(255,176,32,.12)' }
};
function accents() {
return Object.keys(ACCENTS).map(function (k) {
return { key: k, label: ACCENTS[k].label, hex: ACCENTS[k].hex };
});
}
function blank() {
return { brandName: '', tagline: '', accent: 'gold', monogram: '', enabled: false };
}
function load(id) {
try { return JSON.parse(fs.readFileSync(file(id), 'utf8')); } catch (e) { return null; }
}
// Monogram from the brand name: first letters of the first two words, or the
// first two characters of a single word. Latin letters and digits only, so a
// name in any script still yields something renderable rather than a blank.
function monogramFor(name) {
const clean = String(name || '').replace(/[^A-Za-z0-9 ]+/g, ' ').trim();
if (!clean) return '';
const words = clean.split(/\s+/).filter(Boolean);
if (words.length >= 2) return (words[0][0] + words[1][0]).toUpperCase();
return words[0].slice(0, 2).toUpperCase();
}
function save(id, body) {
const rec = blank();
rec.brandName = String((body && body.brandName) || '').replace(/\s+/g, ' ').trim().slice(0, 32);
rec.tagline = String((body && body.tagline) || '').replace(/\s+/g, ' ').trim().slice(0, 60);
rec.accent = ACCENTS[(body && body.accent)] ? body.accent : 'gold';
rec.monogram = monogramFor(rec.brandName);
// A brand with no name is not a brand — treat it as off rather than
// rendering an empty header where the team identity used to be.
rec.enabled = rec.brandName.length >= 2;
rec.id = Number(id);
rec.updatedAt = new Date().toISOString();
try { fs.writeFileSync(file(id), JSON.stringify(rec)); } catch (e) {}
return rec;
}
function clear(id) {
try { fs.unlinkSync(file(id)); } catch (e) {}
return blank();
}
// What the page renderer needs. Returns null when a position has no brand or is
// below the level — callers then fall back to the team identity, so a member
// who drops below L6 simply reverts rather than breaking.
function forPage(id, level) {
if (Number(level) < MIN_LEVEL) return null;
const rec = load(id);
if (!rec || !rec.enabled || !rec.brandName) return null;
const a = ACCENTS[rec.accent] || ACCENTS.gold;
return {
brandName: rec.brandName,
tagline: rec.tagline,
monogram: rec.monogram || monogramFor(rec.brandName),
accent: a.hex,
accentSoft: a.soft
};
}
module.exports = { init, MIN_LEVEL, ACCENTS, accents, blank, load, save, clear, forPage, monogramFor };