// 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 };