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
+77
View File
@@ -0,0 +1,77 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><meta name="description" content="Co-Branded Pages — put your own name, monogram and colour on every page you build."><title>Co-Branded Pages | The Circle Suite</title><link rel="icon" type="image/png" href="/favicon.png"><link rel="stylesheet" href="/styles.css">
<style>
.bw{max-width:800px;margin:0 auto;padding:24px 20px 70px}
.bw h1{font-size:clamp(26px,4.5vw,36px);margin:6px 0 8px}
.bw .lede{color:var(--muted);line-height:1.6;margin-bottom:18px}
#bGate{display:none;border:2px solid var(--gold);border-radius:12px;padding:16px 20px;margin-bottom:18px;line-height:1.6}
.b-card{background:var(--card,#0d2236);border:1px solid var(--line,#24425d);border-radius:14px;padding:18px 20px;margin-bottom:16px}
label.b-lab{display:block;font-weight:700;font-size:14px;margin:16px 0 5px}
label.b-lab:first-of-type{margin-top:0}
.b-hint{color:var(--muted);font-size:12.5px;line-height:1.5;margin-bottom:7px}
.b-card input[type=text]{width:100%;background:var(--bg,#0b1d2e);border:1px solid var(--line);border-radius:10px;
color:var(--text);padding:11px 13px;font:inherit;font-size:15px}
.b-swatches{display:flex;flex-wrap:wrap;gap:9px}
.b-sw{width:44px;height:44px;border-radius:50%;border:3px solid transparent;cursor:pointer;padding:0;
box-shadow:0 0 0 1px var(--line) inset}
.b-sw.on{border-color:var(--text)}
.b-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:18px}
#bErr{display:none;color:#f0a05a;margin-top:12px;font-size:14px;line-height:1.55}
#bOk{display:none;border:2px solid var(--teal);border-radius:12px;padding:14px 18px;margin-top:14px;line-height:1.6}
.b-prev{border:1px solid var(--line);border-radius:14px;overflow:hidden;margin-top:6px}
.b-prevbar{display:flex;align-items:center;gap:11px;padding:14px 18px;background:#071421}
.b-mono{display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:50%;
font-weight:900;font-size:16px;letter-spacing:.5px}
.b-prevbar .nm{font-weight:800;font-size:16px;line-height:1.2}
.b-prevbar .tg{font-size:12px;color:var(--muted)}
.b-prevbody{padding:16px 18px;background:#071421;border-top:1px solid var(--line)}
.b-prevbody h3{font-size:20px;margin:0 0 6px}
.b-prevbody .btn{pointer-events:none;margin-top:10px}
.b-note{margin-top:22px;font-size:12.5px;color:var(--muted);line-height:1.6;border-top:1px solid var(--line);padding-top:12px}
</style></head>
<body>
<header class="wrap nav"><a class="brand" href="/suite"><img class="brand-mark" src="/logo.jpg" alt="The RM Circle" width="42" height="42"><span><span id="brandName">RM Circle</span><small>Co-Branded Pages</small></span></a><div class="nav-actions"><a class="btn btn-secondary hide-mobile" href="/suite">← The Suite</a><a class="btn btn-primary" href="/suite/page">My pages</a></div></header>
<main class="bw">
<div class="eyebrow" style="color:var(--gold)">CIRCLE SUITE · FASTIGIUM (LEVEL 6)</div>
<h1>Co-Branded <span class="gold">Pages.</span></h1>
<p class="lede">Every page you build carries the team's identity by default. From Fastigium it carries yours instead — your name, your monogram, your colour — so what you send people stops looking like a page on someone else's site.</p>
<div id="bGate"></div>
<div id="bBody" style="display:none">
<div class="b-card">
<label class="b-lab" for="bName">Your brand name</label>
<div class="b-hint">What people should see at the top of your pages. Your own name works perfectly — "Marty Bostick" is a brand.</div>
<input type="text" id="bName" maxlength="32" placeholder="Bostick Builds">
<label class="b-lab" for="bTag">One line under it</label>
<div class="b-hint">Optional. A short line about what you do, not a slogan.</div>
<input type="text" id="bTag" maxlength="60" placeholder="Real crypto, real people">
<label class="b-lab">Your colour</label>
<div class="b-hint">Replaces the gold on your pages. Every option here is checked to stay readable on the dark background.</div>
<div class="b-swatches" id="bSwatches"></div>
<label class="b-lab">How it will look</label>
<div class="b-prev" id="bPrev">
<div class="b-prevbar"><span class="b-mono" id="bMono">—</span>
<span><div class="nm" id="bPrevName">Your brand</div><div class="tg" id="bPrevTag">A personal invitation</div></span></div>
<div class="b-prevbody">
<h3 id="bPrevHead">You already know two people. <span id="bPrevGold">That's the whole job.</span></h3>
<span class="btn btn-primary" id="bPrevBtn">Join you →</span>
</div>
</div>
<div class="b-row">
<button id="bSave" class="btn btn-primary">Save my branding</button>
<button id="bClear" class="btn btn-secondary">Use team branding</button>
</div>
<div id="bErr"></div>
<div id="bOk"></div>
</div>
</div>
<p class="b-note"><strong style="color:var(--text)">What branding does not change:</strong> the disclaimers at the foot of every page — independent team resource, no income guaranteed, cryptocurrency involves risk — are ours and stay exactly as they are. This is co-branding, not white-labelling; anyone reading your page can still see what they're looking at. <br><br>There's no logo upload on purpose: your monogram is generated from your brand name, so there's no image to moderate and nothing of yours hosted on our domain.</p>
</main>
<script src="/suite-brand.js"></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+144
View File
@@ -0,0 +1,144 @@
// Co-Branded Pages client. Everything previews live, because the whole value of
// this tool is seeing what your pages will look like before you commit.
(function () {
'use strict';
var $ = function (id) { return document.getElementById(id); };
var state = { accents: [], accent: 'gold' };
function gate(msg) {
$('bGate').style.display = 'block';
$('bGate').innerHTML = msg;
}
function hexFor(key) {
var a = state.accents.filter(function (x) { return x.key === key; })[0];
return a ? a.hex : '#f3be43';
}
// Same rule the server uses, so the preview cannot disagree with what gets
// saved: initials of the first two words, or the first two characters.
function monogram(name) {
var clean = String(name || '').replace(/[^A-Za-z0-9 ]+/g, ' ').trim();
if (!clean) return '';
var w = clean.split(/\s+/).filter(Boolean);
if (w.length >= 2) return (w[0][0] + w[1][0]).toUpperCase();
return w[0].slice(0, 2).toUpperCase();
}
function renderSwatches() {
var host = $('bSwatches');
host.innerHTML = '';
state.accents.forEach(function (a) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'b-sw' + (a.key === state.accent ? ' on' : '');
b.style.background = a.hex;
b.title = a.label;
b.setAttribute('aria-label', a.label);
b.addEventListener('click', function () { state.accent = a.key; renderSwatches(); preview(); });
host.appendChild(b);
});
}
function preview() {
var name = $('bName').value.trim();
var tag = $('bTag').value.trim();
var hex = hexFor(state.accent);
var mono = monogram(name);
$('bMono').textContent = mono || '—';
$('bMono').style.color = hex;
$('bMono').style.border = '2px solid ' + hex;
$('bMono').style.background = hex + '22';
$('bPrevName').textContent = name || 'Your brand';
$('bPrevTag').textContent = tag || 'A personal invitation';
$('bPrevGold').style.color = hex;
var btn = $('bPrevBtn');
btn.style.background = hex;
btn.style.borderColor = hex;
btn.style.color = '#071421';
}
async function save() {
$('bErr').style.display = 'none';
$('bOk').style.display = 'none';
var name = $('bName').value.trim();
if (name.length < 2) {
$('bErr').textContent = 'Give your brand a name — two characters or more.';
$('bErr').style.display = 'block';
return;
}
$('bSave').disabled = true;
try {
var r = await fetch('/api/public/suite-brand', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ brandName: name, tagline: $('bTag').value.trim(), accent: state.accent })
});
var d = await r.json();
if (!r.ok) {
$('bErr').textContent = d.error || 'That did not save.';
$('bErr').style.display = 'block';
} else {
$('bOk').innerHTML = '✅ <b>Saved.</b> Every page you have built now carries your branding — ' +
'<a href="/suite/page" style="color:var(--gold);font-weight:700">go and look at one</a>.';
$('bOk').style.display = 'block';
}
} catch (e) {
$('bErr').textContent = 'Connection hiccup — try again.';
$('bErr').style.display = 'block';
}
$('bSave').disabled = false;
}
async function clearBrand() {
if (!window.confirm('Go back to team branding on all your pages?')) return;
try {
var r = await fetch('/api/public/suite-brand', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clear: true })
});
var d = await r.json();
if (r.ok) {
$('bName').value = ''; $('bTag').value = '';
state.accent = 'gold';
renderSwatches(); preview();
$('bOk').innerHTML = 'Back to team branding on all your pages.';
$('bOk').style.display = 'block';
}
} catch (e) {}
}
async function boot() {
try {
var r = await fetch('/api/public/suite-brand');
if (r.status === 401) { gate('You’re not signed in yet. <a href="/suite" style="color:var(--gold);font-weight:700">Open the Suite</a> and connect the wallet that holds your position, then come back.'); return; }
if (r.status === 403) {
var g = await r.json();
gate((g.error || 'Not open for this position yet.') + ' <a href="/suite" style="color:var(--gold);font-weight:700">See your Suite</a>.');
return;
}
if (!r.ok) return;
var d = await r.json();
state.accents = d.accents || [];
var b = d.brand || {};
state.accent = b.accent || 'gold';
$('bName').value = b.brandName || '';
$('bTag').value = b.tagline || '';
$('bBody').style.display = 'block';
renderSwatches();
preview();
if (!(d.pages || []).length) {
$('bOk').innerHTML = 'You have no pages yet — <a href="/suite/page" style="color:var(--gold);font-weight:700">build one</a> and your branding will be on it.';
$('bOk').style.display = 'block';
}
} catch (e) {}
}
document.addEventListener('DOMContentLoaded', function () {
boot();
$('bName').addEventListener('input', preview);
$('bTag').addEventListener('input', preview);
$('bSave').addEventListener('click', save);
$('bClear').addEventListener('click', clearBrand);
});
})();
+1
View File
@@ -20,6 +20,7 @@
{ lv: 4, ico: '🗣️', name: 'Voice Profile', desc: 'Teach the Copy Engine and Email Engine how you actually talk, so everything they write for you stops sounding like a company.', href: '/suite/voice', live: true }, { lv: 4, ico: '🗣️', name: 'Voice Profile', desc: 'Teach the Copy Engine and Email Engine how you actually talk, so everything they write for you stops sounding like a company.', href: '/suite/voice', live: true },
{ lv: 5, ico: '🔬', name: 'Split Tester', desc: 'Run two or three versions of an ad head to head and find out which wording people actually click. Apex is where your impressions jump to 50,000 — enough for the answer to mean something.', href: '/suite/split', live: true }, { lv: 5, ico: '🔬', name: 'Split Tester', desc: 'Run two or three versions of an ad head to head and find out which wording people actually click. Apex is where your impressions jump to 50,000 — enough for the answer to mean something.', href: '/suite/split', live: true },
{ lv: 1, ico: '🚦', name: 'Traffic Desk', desc: 'Syndicated network display advertising — run banners or AI-written text ads on the team’s own ad network. Monthly impressions scale with your level: 2,500 at Scintilla up to 150,000 at Corona.', href: '/suite/traffic', live: true }, { lv: 1, ico: '🚦', name: 'Traffic Desk', desc: 'Syndicated network display advertising — run banners or AI-written text ads on the team’s own ad network. Monthly impressions scale with your level: 2,500 at Scintilla up to 150,000 at Corona.', href: '/suite/traffic', live: true },
{ lv: 6, ico: '🎨', name: 'Co-Branded Pages', desc: 'Your own name, monogram and colour on every page you build — so it stops looking like a page on the team site and starts looking like yours.', href: '/suite/brand', live: true },
{ lv: 6, ico: '🏭', name: 'Funnel Factory', desc: 'A separate hosted landing page for every audience you talk to — each on its own address, each one something you can point a different ad at.', href: '/suite/funnel', live: true }, { lv: 6, ico: '🏭', name: 'Funnel Factory', desc: 'A separate hosted landing page for every audience you talk to — each on its own address, each one something you can point a different ad at.', href: '/suite/funnel', live: true },
{ lv: 7, ico: '🎁', name: 'Team Grants', desc: 'Hand Suite capacity down into your own organisation — impressions, generations, page builds. Your level stops being something you bought for yourself and becomes something you give your team.', href: '/suite/grants', live: true }, { lv: 7, ico: '🎁', name: 'Team Grants', desc: 'Hand Suite capacity down into your own organisation — impressions, generations, page builds. Your level stops being something you bought for yourself and becomes something you give your team.', href: '/suite/grants', live: true },
{ lv: 8, ico: '🛰️', name: 'Network Intelligence', desc: 'What is actually working across every ad the whole team runs — best angles, headlines and creative, pooled and anonymised. Nobody can buy this; it is the team\'s own accumulated result, and it compounds every month.', href: '/suite/intel', live: true }, { lv: 8, ico: '🛰️', name: 'Network Intelligence', desc: 'What is actually working across every ad the whole team runs — best angles, headlines and creative, pooled and anonymised. Nobody can buy this; it is the team\'s own accumulated result, and it compounds every month.', href: '/suite/intel', live: true },
+31 -1
View File
@@ -31,6 +31,7 @@ const suiteLeader = require('./suite-leader');
const suiteFounder = require('./suite-founder'); suiteFounder.init({ dataDir: DATA_DIR }); const suiteFounder = require('./suite-founder'); suiteFounder.init({ dataDir: DATA_DIR });
const suiteGrants = require('./suite-grants'); suiteGrants.init({ dataDir: DATA_DIR }); const suiteGrants = require('./suite-grants'); suiteGrants.init({ dataDir: DATA_DIR });
const suiteIntel = require('./suite-intel'); suiteIntel.init({ dataDir: DATA_DIR }); const suiteIntel = require('./suite-intel'); suiteIntel.init({ dataDir: DATA_DIR });
const suiteBrand = require('./suite-brand'); suiteBrand.init({ dataDir: DATA_DIR });
const tgbot = require('./tgbot'); const tgbot = require('./tgbot');
tgbot.init({ dataDir: DATA_DIR, chain, getConfig, messages, baseUrl: 'https://rmcircle.team' }); tgbot.init({ dataDir: DATA_DIR, chain, getConfig, messages, baseUrl: 'https://rmcircle.team' });
const SESSION_TTL = 8 * 60 * 60 * 1000; const SESSION_TTL = 8 * 60 * 60 * 1000;
@@ -709,6 +710,12 @@ async function handleApi(req,res,pathname){
res.writeHead(302,securityHeaders({'Location':'/p/'+pid,'Cache-Control':'no-store'})); res.writeHead(302,securityHeaders({'Location':'/p/'+pid,'Cache-Control':'no-store'}));
return res.end(); return res.end();
} }
// Co-branding (L6) is resolved here, against the live contract level, so a
// position that falls below Fastigium reverts to team identity by itself.
try{
const owner=await chain.memberPublic(rec.id).catch(()=>null);
if(owner&&owner.registered){ const br=suiteBrand.forPage(rec.id,owner.level); if(br) rec.brand=br; }
}catch(err){}
const html=suitePages.render(rec); const html=suitePages.render(rec);
res.writeHead(200,securityHeaders({'Content-Type':'text/html; charset=utf-8','Cache-Control':'public, max-age=120'})); res.writeHead(200,securityHeaders({'Content-Type':'text/html; charset=utf-8','Cache-Control':'public, max-age=120'}));
return res.end(html); return res.end(html);
@@ -724,6 +731,12 @@ async function handleApi(req,res,pathname){
res.writeHead(302,securityHeaders({'Location':'/join/'+pid,'Cache-Control':'no-store'})); res.writeHead(302,securityHeaders({'Location':'/join/'+pid,'Cache-Control':'no-store'}));
return res.end(); return res.end();
} }
// Co-branding (L6) is resolved here, against the live contract level, so a
// position that falls below Fastigium reverts to team identity by itself.
try{
const owner=await chain.memberPublic(rec.id).catch(()=>null);
if(owner&&owner.registered){ const br=suiteBrand.forPage(rec.id,owner.level); if(br) rec.brand=br; }
}catch(err){}
const html=suitePages.render(rec); const html=suitePages.render(rec);
// Member pages must ALWAYS be viewable inside a frame — the builder previews // Member pages must ALWAYS be viewable inside a frame — the builder previews
// them, and members share them into contexts that embed. Never send // them, and members share them into contexts that embed. Never send
@@ -1144,6 +1157,23 @@ async function handleApi(req,res,pathname){
return fs.createReadStream(vfile).pipe(res); return fs.createReadStream(vfile).pipe(res);
} }
// -- Co-branded pages (level 6) -------------------------------------------
if(pathname==='/api/public/suite-brand'){
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup - try again.',code:500}));
if(e.error)return json(res,e.code||500,{error:e.error});
if(!e.inOrg||!e.allowed)return json(res,403,{error:'The Circle Suite is not open for this position yet.'});
if(Number(e.d.level)<suiteBrand.MIN_LEVEL)return json(res,403,{error:'Co-branded pages unlock at Fastigium (level 6).',minLevel:suiteBrand.MIN_LEVEL});
if(req.method==='GET'){
return json(res,200,{brand:suiteBrand.load(e.d.id)||suiteBrand.blank(),accents:suiteBrand.accents(),
pages:suitePages.list(e.d.id),level:e.d.level,id:e.d.id});
}
if(req.method==='POST'){
const b=await bodyJson(req)||{};
if(b.clear){ return json(res,200,{brand:suiteBrand.clear(e.d.id),cleared:true}); }
return json(res,200,{brand:suiteBrand.save(e.d.id,b),saved:true});
}
}
if(req.method==='POST'&&pathname==='/api/public/suite-traffic-stop'){ if(req.method==='POST'&&pathname==='/api/public/suite-traffic-stop'){
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup — try again.',code:500})); const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup — try again.',code:500}));
if(e.error)return json(res,e.code||500,{error:e.error}); if(e.error)return json(res,e.code||500,{error:e.error});
@@ -1471,7 +1501,7 @@ const server=http.createServer(async(req,res)=>{
if((mj=pathname.match(/^\/join\/(\d{1,15})$/)))return serveMemberPage(req,res,path.join(PUBLIC_DIR,'join.html'),'join',mj[1]); if((mj=pathname.match(/^\/join\/(\d{1,15})$/)))return serveMemberPage(req,res,path.join(PUBLIC_DIR,'join.html'),'join',mj[1]);
} }
let file; let file;
if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/app'||pathname==='/app/')file=path.join(PUBLIC_DIR,'app.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/privacy'||pathname==='/privacy/')file=path.join(PUBLIC_DIR,'privacy.html');else if(pathname==='/refunds'||pathname==='/refunds/')file=path.join(PUBLIC_DIR,'refunds.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/fast-start'||pathname==='/fast-start/')file=path.join(PUBLIC_DIR,'fast-start.html');else if(pathname==='/flyers'||pathname==='/flyers/')file=path.join(PUBLIC_DIR,'flyers.html');else if(pathname==='/weekly-rhythm'||pathname==='/weekly-rhythm/')file=path.join(PUBLIC_DIR,'weekly-rhythm.html');else if(pathname==='/generation-pay'||pathname==='/generation-pay/')file=path.join(PUBLIC_DIR,'generation-pay.html');else if(pathname==='/suite'||pathname==='/suite/')file=path.join(PUBLIC_DIR,'suite.html');else if(pathname==='/suite/copy'||pathname==='/suite/copy/')file=path.join(PUBLIC_DIR,'suite-copy.html');else if(pathname==='/suite/page'||pathname==='/suite/page/')file=path.join(PUBLIC_DIR,'suite-page.html');else if(pathname==='/suite/email'||pathname==='/suite/email/')file=path.join(PUBLIC_DIR,'suite-email.html');else if(pathname==='/suite/video'||pathname==='/suite/video/')file=path.join(PUBLIC_DIR,'suite-video.html');else if(pathname==='/suite/traffic'||pathname==='/suite/traffic/')file=path.join(PUBLIC_DIR,'suite-traffic.html');else if(pathname==='/suite/voice'||pathname==='/suite/voice/')file=path.join(PUBLIC_DIR,'suite-voice.html');else if(pathname==='/suite/split'||pathname==='/suite/split/')file=path.join(PUBLIC_DIR,'suite-split.html');else if(pathname==='/suite/funnel'||pathname==='/suite/funnel/')file=path.join(PUBLIC_DIR,'suite-funnel.html');else if(pathname==='/suite/leader'||pathname==='/suite/leader/')file=path.join(PUBLIC_DIR,'suite-leader.html');else if(pathname==='/suite/founder'||pathname==='/suite/founder/')file=path.join(PUBLIC_DIR,'suite-founder.html');else if(pathname==='/wallet-check'||pathname==='/wallet-check/')file=path.join(PUBLIC_DIR,'wallet-check.html');else if(pathname==='/suite/grants'||pathname==='/suite/grants/')file=path.join(PUBLIC_DIR,'suite-grants.html');else if(pathname==='/suite/intel'||pathname==='/suite/intel/')file=path.join(PUBLIC_DIR,'suite-intel.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/join-now'});return res.end();}else{ if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/app'||pathname==='/app/')file=path.join(PUBLIC_DIR,'app.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/privacy'||pathname==='/privacy/')file=path.join(PUBLIC_DIR,'privacy.html');else if(pathname==='/refunds'||pathname==='/refunds/')file=path.join(PUBLIC_DIR,'refunds.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/fast-start'||pathname==='/fast-start/')file=path.join(PUBLIC_DIR,'fast-start.html');else if(pathname==='/flyers'||pathname==='/flyers/')file=path.join(PUBLIC_DIR,'flyers.html');else if(pathname==='/weekly-rhythm'||pathname==='/weekly-rhythm/')file=path.join(PUBLIC_DIR,'weekly-rhythm.html');else if(pathname==='/generation-pay'||pathname==='/generation-pay/')file=path.join(PUBLIC_DIR,'generation-pay.html');else if(pathname==='/suite'||pathname==='/suite/')file=path.join(PUBLIC_DIR,'suite.html');else if(pathname==='/suite/copy'||pathname==='/suite/copy/')file=path.join(PUBLIC_DIR,'suite-copy.html');else if(pathname==='/suite/page'||pathname==='/suite/page/')file=path.join(PUBLIC_DIR,'suite-page.html');else if(pathname==='/suite/email'||pathname==='/suite/email/')file=path.join(PUBLIC_DIR,'suite-email.html');else if(pathname==='/suite/video'||pathname==='/suite/video/')file=path.join(PUBLIC_DIR,'suite-video.html');else if(pathname==='/suite/traffic'||pathname==='/suite/traffic/')file=path.join(PUBLIC_DIR,'suite-traffic.html');else if(pathname==='/suite/voice'||pathname==='/suite/voice/')file=path.join(PUBLIC_DIR,'suite-voice.html');else if(pathname==='/suite/split'||pathname==='/suite/split/')file=path.join(PUBLIC_DIR,'suite-split.html');else if(pathname==='/suite/funnel'||pathname==='/suite/funnel/')file=path.join(PUBLIC_DIR,'suite-funnel.html');else if(pathname==='/suite/leader'||pathname==='/suite/leader/')file=path.join(PUBLIC_DIR,'suite-leader.html');else if(pathname==='/suite/founder'||pathname==='/suite/founder/')file=path.join(PUBLIC_DIR,'suite-founder.html');else if(pathname==='/wallet-check'||pathname==='/wallet-check/')file=path.join(PUBLIC_DIR,'wallet-check.html');else if(pathname==='/suite/brand'||pathname==='/suite/brand/')file=path.join(PUBLIC_DIR,'suite-brand.html');else if(pathname==='/suite/grants'||pathname==='/suite/grants/')file=path.join(PUBLIC_DIR,'suite-grants.html');else if(pathname==='/suite/intel'||pathname==='/suite/intel/')file=path.join(PUBLIC_DIR,'suite-intel.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/join-now'});return res.end();}else{
const safe=path.normalize(pathname).replace(/^([.][.][/\\])+/, '').replace(/^[/\\]+/,'');file=path.join(PUBLIC_DIR,safe);if(!file.startsWith(PUBLIC_DIR))file=''; const safe=path.normalize(pathname).replace(/^([.][.][/\\])+/, '').replace(/^[/\\]+/,'');file=path.join(PUBLIC_DIR,safe);if(!file.startsWith(PUBLIC_DIR))file='';
} }
if(file&&staticFile(req,res,file))return;return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404); if(file&&staticFile(req,res,file))return;return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404);
+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 };
+16 -1
View File
@@ -164,6 +164,10 @@ function goldTail(text) {
function render(rec) { function render(rec) {
const a = ANGLES[rec.angle] || ANGLES.overview; const a = ANGLES[rec.angle] || ANGLES.overview;
// Co-branding is resolved by the caller and passed on the record, so this
// module stays free of level logic. Absent = team identity, which is also
// what a member who drops below L6 gets back automatically.
const b = rec.brand || null;
// The button and the QR go to DIFFERENT places on purpose. // The button and the QR go to DIFFERENT places on purpose.
// //
// `?v=<angle>` is not a normal landing page — it is a SQUEEZE step that hides // `?v=<angle>` is not a normal landing page — it is a SQUEEZE step that hides
@@ -197,6 +201,14 @@ function render(rec) {
// subhead — but NOT the story below, because centred long-form prose is // subhead — but NOT the story below, because centred long-form prose is
// measurably harder to read: every line starts in a different place. // measurably harder to read: every line starts in a different place.
'.pp .by,.pp h1,.pp .sub{text-align:center}' + '.pp .by,.pp h1,.pp .sub{text-align:center}' +
// Co-branding (L6). The accent overrides --gold for THIS page only, so
// every gold detail follows the member's colour without touching the
// site stylesheet. The footer disclaimers are rendered below and keep
// their own muted colour — branding never restyles the risk wording.
(b ? ':root{--gold:' + b.accent + '}'
+ '.brand .mono{display:inline-flex;align-items:center;justify-content:center;width:42px;height:42px;'
+ 'border-radius:50%;background:' + b.accentSoft + ';border:2px solid ' + b.accent + ';'
+ 'color:' + b.accent + ';font-weight:900;font-size:15px;letter-spacing:.5px}' : '') +
'.pp h1{font-size:clamp(30px,6vw,48px);line-height:1.06;letter-spacing:-.8px;margin:8px 0 12px;text-wrap:balance}' + '.pp h1{font-size:clamp(30px,6vw,48px);line-height:1.06;letter-spacing:-.8px;margin:8px 0 12px;text-wrap:balance}' +
'.pp h1 .gold{color:var(--gold)}' + '.pp h1 .gold{color:var(--gold)}' +
'.pp .sub{color:#dbe6ef;font-size:clamp(19px,2.4vw,22px);line-height:1.55;margin:0 auto 24px;max-width:46ch}' + '.pp .sub{color:#dbe6ef;font-size:clamp(19px,2.4vw,22px);line-height:1.55;margin:0 auto 24px;max-width:46ch}' +
@@ -216,7 +228,10 @@ function render(rec) {
'.pp .qr svg{width:100%;height:100%}' + '.pp .qr svg{width:100%;height:100%}' +
'.pp .foot{margin-top:26px;padding-top:14px;border-top:1px solid var(--line);font-size:12.5px;color:var(--muted);line-height:1.6;text-align:center}' + '.pp .foot{margin-top:26px;padding-top:14px;border-top:1px solid var(--line);font-size:12.5px;color:var(--muted);line-height:1.6;text-align:center}' +
'</style></head><body>' + '</style></head><body>' +
'<header class="wrap nav"><a class="brand" href="/"><img class="brand-mark" src="/logo.jpg" alt="The RM Circle" width="42" height="42"><span><span>RM Circle</span><small>A personal invitation</small></span></a></header>' + (b
? '<header class="wrap nav"><span class="brand"><span class="mono">' + esc(b.monogram) + '</span>'
+ '<span><span>' + esc(b.brandName) + '</span><small>' + esc(b.tagline || 'A personal invitation') + '</small></span></span></header>'
: '<header class="wrap nav"><a class="brand" href="/"><img class="brand-mark" src="/logo.jpg" alt="The RM Circle" width="42" height="42"><span><span>RM Circle</span><small>A personal invitation</small></span></a></header>') +
'<main class="pp">' + '<main class="pp">' +
'<div class="by">Shared by <b>' + who + '</b> · Member #' + esc(rec.id) + '</div>' + '<div class="by">Shared by <b>' + who + '</b> · Member #' + esc(rec.id) + '</div>' +
'<h1>' + goldTail(c.headline || '') + '</h1>' + '<h1>' + goldTail(c.headline || '') + '</h1>' +
+2 -2
View File
@@ -15,7 +15,7 @@ const LEVEL_TOOLS = {
3: { name: 'Fabrica', live: true, short: 'the Email Engine and Video Maker — full sequences in the team voice, and the promo videos rendered with your own end card' }, 3: { name: 'Fabrica', live: true, short: 'the Email Engine and Video Maker — full sequences in the team voice, and the promo videos rendered with your own end card' },
4: { name: 'Culmen', live: true, short: 'your Voice Profile — the Copy Engine and Email Engine start writing the way you actually talk' }, 4: { name: 'Culmen', live: true, short: 'your Voice Profile — the Copy Engine and Email Engine start writing the way you actually talk' },
5: { name: 'Apex', live: true, short: 'the Split Tester, and your ad allowance jumps to 50,000 impressions a month' }, 5: { name: 'Apex', live: true, short: 'the Split Tester, and your ad allowance jumps to 50,000 impressions a month' },
6: { name: 'Fastigium', live: true, short: 'the Funnel Factory — a separate hosted landing page for every audience you talk to' }, 6: { name: 'Fastigium', live: true, short: 'Co-Branded Pages and the Funnel Factory — your own name, monogram and colour on every page you build' },
7: { name: 'Vertex', live: true, short: 'Leader Ops and Team Grants — your whole organisation triaged live from the contract, plus the ability to hand Suite capacity down to your own people' }, 7: { name: 'Vertex', live: true, short: 'Leader Ops and Team Grants — your whole organisation triaged live from the contract, plus the ability to hand Suite capacity down to your own people' },
8: { name: 'Corona', live: true, short: 'the Founder Desk and Network Intelligence — a whole week of promotion in one pass, API access, and what is actually working across every ad the team runs' } 8: { name: 'Corona', live: true, short: 'the Founder Desk and Network Intelligence — a whole week of promotion in one pass, API access, and what is actually working across every ad the team runs' }
}; };
@@ -27,7 +27,7 @@ const TERSE = {
3: 'the Email Engine + Video Maker', 3: 'the Email Engine + Video Maker',
4: 'your own Voice Profile', 4: 'your own Voice Profile',
5: 'the Split Tester + 50k impressions', 5: 'the Split Tester + 50k impressions',
6: 'the Funnel Factory', 6: 'Co-Branded Pages',
7: 'Leader Ops + Team Grants', 7: 'Leader Ops + Team Grants',
8: 'the Founder Desk + Network Intelligence' 8: 'the Founder Desk + Network Intelligence'
}; };