diff --git a/public/suite-brand.html b/public/suite-brand.html new file mode 100644 index 0000000..641e28e --- /dev/null +++ b/public/suite-brand.html @@ -0,0 +1,77 @@ +Co-Branded Pages | The Circle Suite + + + +
+
CIRCLE SUITE · FASTIGIUM (LEVEL 6)
+

Co-Branded Pages.

+

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.

+ +
+ + + +

What branding does not change: 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.

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.

+
+ + + diff --git a/public/suite-brand.js b/public/suite-brand.js new file mode 100644 index 0000000..696d471 --- /dev/null +++ b/public/suite-brand.js @@ -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 = '✅ Saved. Every page you have built now carries your branding — ' + + 'go and look at one.'; + $('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. Open the Suite 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.') + ' See your Suite.'); + 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 — build one 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); + }); +})(); diff --git a/public/suite.js b/public/suite.js index ba49a32..4d0c05e 100644 --- a/public/suite.js +++ b/public/suite.js @@ -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: 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: 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: 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 }, diff --git a/server.js b/server.js index 5e384d8..1f7a48f 100644 --- a/server.js +++ b/server.js @@ -31,6 +31,7 @@ const suiteLeader = require('./suite-leader'); const suiteFounder = require('./suite-founder'); suiteFounder.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 suiteBrand = require('./suite-brand'); suiteBrand.init({ dataDir: DATA_DIR }); const tgbot = require('./tgbot'); tgbot.init({ dataDir: DATA_DIR, chain, getConfig, messages, baseUrl: 'https://rmcircle.team' }); 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'})); 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); res.writeHead(200,securityHeaders({'Content-Type':'text/html; charset=utf-8','Cache-Control':'public, max-age=120'})); 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'})); 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); // Member pages must ALWAYS be viewable inside a frame — the builder previews // 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); } + // -- 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)({error:'Chain read hiccup — try again.',code:500})); 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]); } 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=''; } if(file&&staticFile(req,res,file))return;return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404); diff --git a/suite-brand.js b/suite-brand.js new file mode 100644 index 0000000..543530f --- /dev/null +++ b/suite-brand.js @@ -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 }; diff --git a/suite-pages.js b/suite-pages.js index 079f68c..bd54638 100644 --- a/suite-pages.js +++ b/suite-pages.js @@ -164,6 +164,10 @@ function goldTail(text) { function render(rec) { 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. // // `?v=` 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 // measurably harder to read: every line starts in a different place. '.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 .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}' + @@ -216,7 +228,10 @@ function render(rec) { '.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}' + '' + - '' + + (b + ? '' + : '') + '
' + '
Shared by ' + who + ' · Member #' + esc(rec.id) + '
' + '

' + goldTail(c.headline || '') + '

' + diff --git a/suite-tools.js b/suite-tools.js index 0663a40..1e749bb 100644 --- a/suite-tools.js +++ b/suite-tools.js @@ -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' }, 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' }, - 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' }, 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', 4: 'your own Voice Profile', 5: 'the Split Tester + 50k impressions', - 6: 'the Funnel Factory', + 6: 'Co-Branded Pages', 7: 'Leader Ops + Team Grants', 8: 'the Founder Desk + Network Intelligence' };