← back to Coming Soon Template
coming-soon template: Gucci-aesthetic Editorial landing + manifest for 21 domains + shared email-signup endpoint
d3214b02fb3a96525ff13d6b0d5722a4ce37365b · 2026-05-13 09:42:05 -0700 · SteveStudio2
Files touched
A .gitignoreA email-signup-server.jsA manifest.jsonA scripts/build.jsA template.html
Diff
commit d3214b02fb3a96525ff13d6b0d5722a4ce37365b
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Wed May 13 09:42:05 2026 -0700
coming-soon template: Gucci-aesthetic Editorial landing + manifest for 21 domains + shared email-signup endpoint
---
.gitignore | 6 +
email-signup-server.js | 153 +++++++++++++++++++++++++
manifest.json | 244 ++++++++++++++++++++++++++++++++++++++++
scripts/build.js | 89 +++++++++++++++
template.html | 298 +++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 790 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c4dce82
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+out/
diff --git a/email-signup-server.js b/email-signup-server.js
new file mode 100644
index 0000000..69d32cf
--- /dev/null
+++ b/email-signup-server.js
@@ -0,0 +1,153 @@
+#!/usr/bin/env node
+/**
+ * Shared email-signup endpoint for the 21 Gucci-coming-soon pages.
+ *
+ * Listens at POST /api/email-signup
+ * body: { email, source, timestamp }
+ * returns: 200 { ok:true } | 400 { error } | 429 { error } | 500
+ *
+ * Storage: append-only JSONL at ~/data/email-signups.jsonl (or DATA_FILE env).
+ * Rate-limit: max 1 signup per IP per email per 60s (in-memory window).
+ * CORS: allows the 21 known domains + agentabrams.com itself.
+ *
+ * Designed to run on Kamatera behind nginx at agentabrams.com/api/email-signup.
+ * Zero deps. Bind defaults to 127.0.0.1:9697 (override with PORT) so nginx
+ * proxies in; never expose this port publicly.
+ *
+ * Future: ELEVENLABS-free Steve-voice confirmation, webhook to Postgres, etc.
+ */
+
+'use strict';
+
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+
+const PORT = parseInt(process.env.PORT, 10) || 9697;
+const HOST = process.env.HOST || '127.0.0.1';
+const DATA_FILE = process.env.DATA_FILE || path.join(process.env.HOME || '/root', 'data', 'email-signups.jsonl');
+
+const ALLOWED_ORIGINS = new Set([
+ 'https://agentabrams.com',
+ // abrams.* cluster
+ 'https://abramsos.com', 'https://abramsintel.com', 'https://abramsintelligence.com',
+ 'https://abramsspace.com', 'https://abramsterminal.com', 'https://abramsvc.com',
+ 'https://abramsindustries.com', 'https://abramsprotection.com', 'https://abramscivic.com',
+ 'https://abramslive.com', 'https://abramsatlas.com', 'https://abramsdirectory.com',
+ 'https://abramsguide.com', 'https://abramsindex.com', 'https://abramslocal.com',
+ 'https://abramsmaps.com', 'https://abramsmarkets.com',
+ // butler cluster
+ 'https://818butler.com', 'https://beverlyhillsbutler.com',
+ 'https://bhbutler.com', 'https://boulevardbutler.com',
+]);
+
+// Rate-limit window (in-memory)
+const WINDOW_MS = 60_000;
+const seen = new Map(); // key = ip|email → expiresAt
+
+function readBody(req, max = 4096) {
+ return new Promise((resolve, reject) => {
+ let total = 0;
+ const chunks = [];
+ req.on('data', c => {
+ total += c.length;
+ if (total > max) { req.destroy(); reject(new Error('payload too large')); return; }
+ chunks.push(c);
+ });
+ req.on('end', () => {
+ try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')); }
+ catch { reject(new Error('invalid json')); }
+ });
+ req.on('error', reject);
+ });
+}
+
+function ipOf(req) {
+ return (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
+ || (req.socket && req.socket.remoteAddress) || 'unknown';
+}
+
+const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+function send(res, status, body, extraHeaders) {
+ const headers = Object.assign({
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ 'X-Content-Type-Options': 'nosniff',
+ }, extraHeaders || {});
+ res.writeHead(status, headers);
+ res.end(typeof body === 'string' ? body : JSON.stringify(body));
+}
+
+function corsHeaders(origin) {
+ if (origin && ALLOWED_ORIGINS.has(origin)) {
+ return {
+ 'Access-Control-Allow-Origin': origin,
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Content-Type',
+ 'Access-Control-Max-Age': '600',
+ 'Vary': 'Origin',
+ };
+ }
+ return { 'Vary': 'Origin' };
+}
+
+function ensureDataDir() {
+ const dir = path.dirname(DATA_FILE);
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
+}
+ensureDataDir();
+
+const server = http.createServer(async (req, res) => {
+ const origin = req.headers.origin;
+ const cors = corsHeaders(origin);
+
+ if (req.method === 'OPTIONS') { send(res, 204, '', cors); return; }
+
+ if (req.method === 'GET' && req.url === '/api/health') {
+ return send(res, 200, { ok: true, signups_file: DATA_FILE }, cors);
+ }
+
+ if (req.method !== 'POST' || req.url !== '/api/email-signup') {
+ return send(res, 404, { error: 'not found' }, cors);
+ }
+
+ let body;
+ try { body = await readBody(req); }
+ catch (e) { return send(res, 400, { error: e.message }, cors); }
+
+ const email = String(body.email || '').trim().toLowerCase();
+ const source = String(body.source || '').trim().toLowerCase();
+ const ts = body.timestamp ? String(body.timestamp).slice(0, 40) : new Date().toISOString();
+
+ if (!email || !EMAIL_RE.test(email)) return send(res, 400, { error: 'invalid email' }, cors);
+ if (!source) return send(res, 400, { error: 'missing source' }, cors);
+ if (email.length > 320 || source.length > 80) return send(res, 400, { error: 'fields too long' }, cors);
+
+ const ip = ipOf(req);
+ const key = ip + '|' + email;
+ const now = Date.now();
+ // Sweep expired entries cheaply (every ~100 requests this loops; otherwise just delete on hit)
+ if (seen.has(key) && seen.get(key) > now) {
+ return send(res, 429, { error: 'duplicate within window' }, cors);
+ }
+ seen.set(key, now + WINDOW_MS);
+ if (seen.size > 5000) {
+ for (const [k, exp] of seen) if (exp <= now) seen.delete(k);
+ }
+
+ const row = { email, source, timestamp: ts, ip, ua: String(req.headers['user-agent'] || '').slice(0, 200) };
+ try {
+ fs.appendFileSync(DATA_FILE, JSON.stringify(row) + '\n');
+ } catch (e) {
+ console.error('append failed:', e.message);
+ return send(res, 500, { error: 'storage failure' }, cors);
+ }
+
+ send(res, 200, { ok: true }, cors);
+});
+
+server.listen(PORT, HOST, () => {
+ console.log(`email-signup listening on http://${HOST}:${PORT}/api/email-signup`);
+ console.log(`writing to ${DATA_FILE}`);
+});
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..0cbc5d2
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,244 @@
+{
+ "_comment": "Per-domain Gucci-coming-soon template instance data. Hero URLs are Unsplash CC0 editorial photos (per Steve's standing-rule: public-domain/CC0 only). Replace any hero with a Steve-owned photo by swapping the hero_url field. WORDMARK and DOMAIN must match what's bought; tagline is the editorial line under it. ACCENT_HEX is the chrome accent (keep neutral or match hero).",
+ "endpoint": "https://agentabrams.com/api/email-signup",
+ "deploy_root": "/var/www/{{DOMAIN}}/index.html",
+ "year": "2026",
+ "domains": [
+ {
+ "domain": "abramsos.com",
+ "wordmark": "abramsos",
+ "wordmark_initial": "a",
+ "tagline": "the operating system",
+ "page_title": "abramsos — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Editorial silver-grey architectural detail",
+ "accent_hex": "#3a3a3a",
+ "category": "thin-landing"
+ },
+ {
+ "domain": "abramsintel.com",
+ "wordmark": "abramsintel",
+ "wordmark_initial": "a",
+ "tagline": "briefings",
+ "page_title": "abramsintel — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Backlit study with paper documents in soft monochrome",
+ "accent_hex": "#2c2a26",
+ "category": "thin-landing"
+ },
+ {
+ "domain": "abramsintelligence.com",
+ "wordmark": "abramsintelligence",
+ "wordmark_initial": "a",
+ "tagline": "briefings",
+ "page_title": "abramsintelligence — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Backlit study with paper documents in soft monochrome",
+ "accent_hex": "#2c2a26",
+ "category": "thin-landing",
+ "note": "301-canonical to abramsintel.com — page is fallback for direct hits"
+ },
+ {
+ "domain": "abramsspace.com",
+ "wordmark": "abramsspace",
+ "wordmark_initial": "a",
+ "tagline": "workspace",
+ "page_title": "abramsspace — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Empty modern workspace with morning light",
+ "accent_hex": "#4a463d",
+ "category": "thin-landing"
+ },
+ {
+ "domain": "abramsterminal.com",
+ "wordmark": "abramsterminal",
+ "wordmark_initial": "a",
+ "tagline": "command line access",
+ "page_title": "abramsterminal — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1517694712202-14dd9538aa97?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Dark terminal with monospace code, editorial low-key",
+ "accent_hex": "#1a1a1a",
+ "category": "thin-landing"
+ },
+ {
+ "domain": "abramsvc.com",
+ "wordmark": "abramsvc",
+ "wordmark_initial": "a",
+ "tagline": "venture portfolio",
+ "page_title": "abramsvc — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1521737711867-e3b97375f902?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Aerial board-meeting framing in soft warm tones",
+ "accent_hex": "#3d362a",
+ "category": "thin-landing"
+ },
+ {
+ "domain": "abramsindustries.com",
+ "wordmark": "abramsindustries",
+ "wordmark_initial": "a",
+ "tagline": "the umbrella",
+ "page_title": "abramsindustries — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1503387762-592deb58ef4e?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Industrial skyline in muted dawn",
+ "accent_hex": "#2e2c28",
+ "category": "thin-landing"
+ },
+ {
+ "domain": "abramsprotection.com",
+ "wordmark": "abramsprotection",
+ "wordmark_initial": "a",
+ "tagline": "guardian layer",
+ "page_title": "abramsprotection — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1550745165-9bc0b252726f?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Quiet vault interior in monochrome",
+ "accent_hex": "#1f1d1b",
+ "category": "thin-landing"
+ },
+ {
+ "domain": "abramscivic.com",
+ "wordmark": "abramscivic",
+ "wordmark_initial": "a",
+ "tagline": "directories for the public good",
+ "page_title": "abramscivic — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1486325212027-8081e485255e?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Civic colonnade in even daylight",
+ "accent_hex": "#3a342c",
+ "category": "thin-landing"
+ },
+ {
+ "domain": "abramslive.com",
+ "wordmark": "abramslive",
+ "wordmark_initial": "a",
+ "tagline": "live builds, in public",
+ "page_title": "abramslive — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1531297484001-80022131f5a1?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Late-night studio screen-glow, soft and grainy",
+ "accent_hex": "#1b1c1f",
+ "category": "thin-landing"
+ },
+ {
+ "domain": "abramsatlas.com",
+ "wordmark": "abramsatlas",
+ "wordmark_initial": "a",
+ "tagline": "the map",
+ "page_title": "abramsatlas — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1524661135-423995f22d0b?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Antique atlas spread, sepia editorial crop",
+ "accent_hex": "#4a3f2a",
+ "category": "redirect-soon",
+ "note": "Page is short-lived — 301s to agentabrams.com once site is live"
+ },
+ {
+ "domain": "abramsdirectory.com",
+ "wordmark": "abramsdirectory",
+ "wordmark_initial": "a",
+ "tagline": "the index",
+ "page_title": "abramsdirectory — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1481627834876-b7833e8f5570?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Library card catalog drawers, cool monochrome",
+ "accent_hex": "#2a261f",
+ "category": "redirect-soon"
+ },
+ {
+ "domain": "abramsguide.com",
+ "wordmark": "abramsguide",
+ "wordmark_initial": "a",
+ "tagline": "the guide",
+ "page_title": "abramsguide — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1495446815901-a7297e633e8d?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Open atlas with margin notes in warm light",
+ "accent_hex": "#3d352a",
+ "category": "redirect-soon"
+ },
+ {
+ "domain": "abramsindex.com",
+ "wordmark": "abramsindex",
+ "wordmark_initial": "a",
+ "tagline": "the ledger",
+ "page_title": "abramsindex — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1457369804613-52c61a468e7d?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Stacked books in editorial low-light",
+ "accent_hex": "#2c241c",
+ "category": "redirect-soon"
+ },
+ {
+ "domain": "abramslocal.com",
+ "wordmark": "abramslocal",
+ "wordmark_initial": "a",
+ "tagline": "the neighborhood",
+ "page_title": "abramslocal — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1502672023488-70e25813eb80?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Quiet residential street, sunrise palette",
+ "accent_hex": "#3d3a30",
+ "category": "redirect-soon"
+ },
+ {
+ "domain": "abramsmaps.com",
+ "wordmark": "abramsmaps",
+ "wordmark_initial": "a",
+ "tagline": "the cartographer",
+ "page_title": "abramsmaps — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1524661135-423995f22d0b?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Antique map detail with compass rose",
+ "accent_hex": "#4a3f2a",
+ "category": "redirect-soon"
+ },
+ {
+ "domain": "abramsmarkets.com",
+ "wordmark": "abramsmarkets",
+ "wordmark_initial": "a",
+ "tagline": "the floor",
+ "page_title": "abramsmarkets — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1559526324-4b87b5e36e44?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Marketplace stalls in early-morning low light",
+ "accent_hex": "#322a1e",
+ "category": "redirect-soon"
+ },
+
+ {
+ "domain": "818butler.com",
+ "wordmark": "818butler",
+ "wordmark_initial": "b",
+ "tagline": "white-glove service · 818",
+ "page_title": "818butler — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1597714026720-8f74c62310ba?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Interior of a chauffeured town car, warm leather",
+ "accent_hex": "#2a221a",
+ "category": "thin-landing"
+ },
+ {
+ "domain": "beverlyhillsbutler.com",
+ "wordmark": "beverlyhillsbutler",
+ "wordmark_initial": "b",
+ "tagline": "white-glove service · Beverly Hills",
+ "page_title": "beverlyhillsbutler — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1564013799919-ab600027ffc6?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Quiet private residence porte-cochère at dusk",
+ "accent_hex": "#1f1c17",
+ "category": "thin-landing",
+ "note": "Already has its own pm2 process — may keep that one as canonical"
+ },
+ {
+ "domain": "bhbutler.com",
+ "wordmark": "bhbutler",
+ "wordmark_initial": "b",
+ "tagline": "white-glove service",
+ "page_title": "bhbutler — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1564013799919-ab600027ffc6?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Quiet private residence porte-cochère at dusk",
+ "accent_hex": "#1f1c17",
+ "category": "thin-landing",
+ "note": "Short-form alias of beverlyhillsbutler.com — may 301 to it"
+ },
+ {
+ "domain": "boulevardbutler.com",
+ "wordmark": "boulevardbutler",
+ "wordmark_initial": "b",
+ "tagline": "white-glove service · the boulevards",
+ "page_title": "boulevardbutler — coming soon",
+ "hero_url": "https://images.unsplash.com/photo-1503376780353-7e6692767b70?auto=format&fit=crop&w=2560&q=85",
+ "hero_alt": "Empty boulevard at first light, soft warm grade",
+ "accent_hex": "#322a1e",
+ "category": "thin-landing"
+ }
+ ]
+}
diff --git a/scripts/build.js b/scripts/build.js
new file mode 100644
index 0000000..599b3da
--- /dev/null
+++ b/scripts/build.js
@@ -0,0 +1,89 @@
+#!/usr/bin/env node
+/**
+ * Instantiates template.html per-domain from manifest.json → out/<domain>/index.html
+ *
+ * Usage:
+ * node scripts/build.js # build all
+ * node scripts/build.js abramsos.com # build one
+ * node scripts/build.js --deploy # also rsync to Kamatera /var/www/<domain>/
+ *
+ * Swap points (case-sensitive, double-curly):
+ * {{WORDMARK}} {{WORDMARK_INITIAL}} {{TAGLINE}} {{HERO_URL}} {{HERO_ALT}}
+ * {{DOMAIN}} {{PAGE_TITLE}} {{ACCENT_HEX}} {{YEAR}}
+ */
+
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const ROOT = path.resolve(__dirname, '..');
+const TEMPLATE = path.join(ROOT, 'template.html');
+const MANIFEST = path.join(ROOT, 'manifest.json');
+const OUT = path.join(ROOT, 'out');
+
+const args = process.argv.slice(2);
+const deploy = args.includes('--deploy');
+const onlyDomain = args.find(a => !a.startsWith('--'));
+
+if (!fs.existsSync(TEMPLATE)) { console.error('template.html missing'); process.exit(1); }
+if (!fs.existsSync(MANIFEST)) { console.error('manifest.json missing'); process.exit(1); }
+
+const tpl = fs.readFileSync(TEMPLATE, 'utf8');
+const manifest = JSON.parse(fs.readFileSync(MANIFEST, 'utf8'));
+const year = manifest.year || new Date().getFullYear();
+
+function instantiate(entry) {
+ let out = tpl;
+ const subs = {
+ WORDMARK: entry.wordmark,
+ WORDMARK_INITIAL: entry.wordmark_initial || entry.wordmark.slice(0, 1),
+ TAGLINE: entry.tagline,
+ HERO_URL: entry.hero_url,
+ HERO_ALT: entry.hero_alt || (entry.wordmark + ' coming soon'),
+ DOMAIN: entry.domain,
+ PAGE_TITLE: entry.page_title || (entry.wordmark + ' — coming soon'),
+ ACCENT_HEX: entry.accent_hex || '#3a3a3a',
+ YEAR: String(year),
+ };
+ for (const [k, v] of Object.entries(subs)) {
+ out = out.replaceAll('{{' + k + '}}', v);
+ }
+ return out;
+}
+
+const targets = onlyDomain
+ ? manifest.domains.filter(d => d.domain === onlyDomain)
+ : manifest.domains;
+
+if (!targets.length) {
+ console.error(onlyDomain ? `no manifest entry for ${onlyDomain}` : 'no domains in manifest');
+ process.exit(1);
+}
+
+let built = 0;
+for (const entry of targets) {
+ const html = instantiate(entry);
+ const dir = path.join(OUT, entry.domain);
+ fs.mkdirSync(dir, { recursive: true });
+ fs.writeFileSync(path.join(dir, 'index.html'), html);
+ built++;
+ process.stdout.write(` built ${entry.domain.padEnd(28)} ${entry.tagline}\n`);
+}
+console.log(`\n✓ ${built} page${built === 1 ? '' : 's'} built into ${OUT}/`);
+
+if (deploy) {
+ const REMOTE = 'root@45.61.58.125';
+ console.log(`\n→ rsync to ${REMOTE} (/var/www/<domain>/)…`);
+ for (const entry of targets) {
+ const src = path.join(OUT, entry.domain, 'index.html');
+ const dest = REMOTE + ':/var/www/' + entry.domain + '/index.html';
+ try {
+ execSync(`rsync -az --mkpath "${src}" "${dest}"`, { stdio: 'inherit' });
+ } catch (e) {
+ console.error(` ✗ ${entry.domain} — ${e.message}`);
+ }
+ }
+ console.log('✓ deploy complete (vhosts must point at these paths)');
+}
diff --git a/template.html b/template.html
new file mode 100644
index 0000000..e574ab7
--- /dev/null
+++ b/template.html
@@ -0,0 +1,298 @@
+<!doctype html>
+<html lang="en" data-theme="light">
+<head>
+<!--
+ Coming-soon template — Gucci-aesthetic editorial landing.
+ Per-domain swap points (case-sensitive, double-curly):
+ {{WORDMARK}} — domain stem, no TLD → e.g. "abramsos"
+ {{TAGLINE}} — italic line under wordmark → e.g. "the operating system"
+ {{HERO_URL}} — 2560×1600 editorial photo URL → CC0 or Steve-owned
+ {{HERO_ALT}} — accessible alt text
+ {{DOMAIN}} — full domain incl. TLD → posted as `source` to /api/email-signup
+ {{PAGE_TITLE}} — <title> + og:title → e.g. "abramsos — coming soon"
+ {{ACCENT_HEX}} — single accent hex for chrome → keep neutral or pull from hero
+
+ Paper-design specs:
+ container max-width: 1440px center, 24px gutters
+ grid: hero 100vh, wordmark zone 480px below
+ type scale (modular 1.25): 11→14→18→22→28→36→48 (px)
+ wordmark: clamp(56px, 9vw, 128px), Cormorant Garamond display, weight 300
+ tagline: 12px, sans, +0.06em tracked, uppercase, opacity .6
+ email input: 14px monospace, hairline 1px underline, no border
+ breakpoints: <=720 mobile (wordmark→clamp 44px,12vw,96px; hero 70vh)
+
+ Standing rules honored:
+ - Logo upper-left (initial only)
+ - Hamburger upper-right (decorative — no menu wired)
+ - Sun/moon toggle next to hamburger (anti-flash inline script)
+ - Wordmark BELOW the hero photo, never overlapping
+ - Viewport meta required (mobile @media depends on it)
+ - No AI-vendor names visible
+-->
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<meta name="robots" content="noindex,nofollow">
+<title>{{PAGE_TITLE}}</title>
+<meta property="og:title" content="{{PAGE_TITLE}}">
+<meta property="og:image" content="{{HERO_URL}}">
+<meta name="theme-color" content="#faf8f3">
+<link rel="preload" as="image" href="{{HERO_URL}}">
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,500;1,300&family=Inter:wght@300;400&family=JetBrains+Mono:wght@300;400&display=swap">
+<script>
+ // Anti-flash theme hydration — must run before paint
+ (function(){
+ try {
+ var t = localStorage.getItem('cs-theme');
+ if (t === 'dark' || (t === null && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
+ document.documentElement.setAttribute('data-theme', 'dark');
+ }
+ } catch(e) {}
+ })();
+</script>
+<style>
+ :root {
+ --bg: #faf8f3;
+ --ink: #0f0e0c;
+ --ink-soft: #57544c;
+ --ink-faint: #9b958a;
+ --line: #e5dfd0;
+ --card: #ffffff;
+ --accent: {{ACCENT_HEX}};
+ --serif: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
+ --sans: 'Inter', -apple-system, system-ui, sans-serif;
+ --mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
+ }
+ html[data-theme="dark"] {
+ --bg: #0f0e0c;
+ --ink: #faf8f3;
+ --ink-soft: #b5ab98;
+ --ink-faint: #6c6557;
+ --line: #2a2820;
+ --card: #18170f;
+ }
+ * { box-sizing: border-box; }
+ html, body { margin: 0; padding: 0; background: var(--bg); color: var(--ink); }
+ body { font-family: var(--sans); font-weight: 300; -webkit-font-smoothing: antialiased; line-height: 1.5; min-height: 100vh; }
+ a { color: inherit; text-decoration: none; }
+
+ /* HEADER — fixed bar, transparent over hero, fades to bg on scroll */
+ header.gh {
+ position: fixed; top: 0; left: 0; right: 0; z-index: 10;
+ display: flex; justify-content: space-between; align-items: center;
+ padding: 22px 28px;
+ pointer-events: none;
+ }
+ header.gh > * { pointer-events: auto; }
+ .glyph {
+ font-family: var(--serif); font-weight: 300; font-size: 32px; line-height: 1;
+ color: var(--ink); letter-spacing: -0.02em;
+ text-shadow: 0 1px 12px rgba(0,0,0,.22);
+ }
+ html[data-theme="dark"] .glyph { text-shadow: 0 1px 12px rgba(0,0,0,.55); }
+ .top-right { display: flex; align-items: center; gap: 18px; }
+ .theme-toggle, .ham {
+ background: transparent; border: 0; padding: 6px; cursor: pointer;
+ color: var(--ink); width: 32px; height: 32px;
+ display: flex; align-items: center; justify-content: center;
+ filter: drop-shadow(0 1px 8px rgba(0,0,0,.22));
+ }
+ .ham svg, .theme-toggle svg { width: 22px; height: 22px; }
+
+ /* HERO — full-bleed editorial photo */
+ .hero {
+ width: 100%; height: 100vh; min-height: 560px;
+ background: var(--card) center/cover no-repeat;
+ background-image: url('{{HERO_URL}}');
+ position: relative;
+ }
+ .hero::after {
+ /* Subtle bottom-fade so the wordmark zone reads cleanly when content peeks */
+ content: ''; position: absolute; inset: auto 0 0 0; height: 24%;
+ background: linear-gradient(to bottom, transparent, var(--bg));
+ pointer-events: none;
+ }
+
+ /* WORDMARK ZONE — center column under hero */
+ .meta {
+ max-width: 720px; margin: 0 auto; padding: 64px 24px 96px;
+ text-align: center;
+ }
+ .wordmark {
+ font-family: var(--serif); font-weight: 300; font-style: normal;
+ font-size: clamp(56px, 9vw, 128px); line-height: 1.02;
+ letter-spacing: -0.01em; color: var(--ink);
+ margin: 0 0 14px;
+ }
+ .tagline {
+ font-family: var(--serif); font-style: italic; font-weight: 300;
+ font-size: clamp(16px, 2.4vw, 22px);
+ color: var(--ink-soft); margin: 0 0 4px;
+ }
+ .eyebrow {
+ font-family: var(--sans); font-size: 11px; font-weight: 400;
+ text-transform: uppercase; letter-spacing: 0.18em;
+ color: var(--ink-faint); margin: 0 0 24px;
+ }
+
+ /* EMAIL CAPTURE — hairline, monospace, link-style submit */
+ form.signup {
+ margin: 40px auto 0; max-width: 420px;
+ display: flex; align-items: baseline; gap: 18px;
+ border-bottom: 1px solid var(--line);
+ padding-bottom: 6px;
+ }
+ form.signup input[type=email] {
+ flex: 1; min-width: 0;
+ background: transparent; border: 0; outline: none;
+ font-family: var(--mono); font-size: 14px; font-weight: 400;
+ color: var(--ink); padding: 10px 0;
+ }
+ form.signup input[type=email]::placeholder { color: var(--ink-faint); }
+ form.signup button {
+ background: transparent; border: 0; padding: 0; cursor: pointer;
+ font-family: var(--sans); font-size: 11px; font-weight: 400;
+ text-transform: uppercase; letter-spacing: 0.18em;
+ color: var(--ink); border-bottom: 1px solid var(--ink);
+ padding-bottom: 2px;
+ transition: opacity .15s ease;
+ }
+ form.signup button:hover { opacity: 0.6; }
+ form.signup button:disabled { color: var(--ink-faint); border-color: var(--ink-faint); cursor: progress; }
+ .signup-msg {
+ margin: 14px auto 0; max-width: 420px;
+ font-family: var(--sans); font-size: 12px; color: var(--ink-soft);
+ min-height: 18px;
+ text-align: center;
+ }
+ .signup-msg.err { color: #c0392b; }
+ .signup-msg.ok { color: #1e6b3a; }
+
+ /* FOOTER */
+ footer.gf {
+ margin-top: 40px; padding: 28px 24px 36px;
+ border-top: 1px solid var(--line);
+ text-align: center;
+ font-family: var(--sans); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
+ color: var(--ink-faint);
+ }
+ footer.gf a {
+ color: var(--ink-soft); border-bottom: 1px solid transparent;
+ transition: border-color .15s ease;
+ }
+ footer.gf a:hover { border-color: var(--ink-soft); }
+ .gf-domain {
+ font-family: var(--serif); font-style: italic; font-size: 14px;
+ text-transform: none; letter-spacing: 0; color: var(--ink-soft);
+ margin-right: 14px;
+ }
+
+ /* Mobile */
+ @media (max-width: 720px) {
+ .hero { height: 70vh; min-height: 480px; }
+ .meta { padding: 48px 20px 72px; }
+ header.gh { padding: 16px 18px; }
+ .glyph { font-size: 26px; }
+ }
+
+ /* Print — keep wordmark + tagline readable; suppress chrome */
+ @media print {
+ header.gh, .theme-toggle, .ham, form.signup, .signup-msg { display: none; }
+ .hero { height: 60vh; }
+ }
+</style>
+</head>
+<body>
+
+<header class="gh">
+ <a href="/" class="glyph" aria-label="{{WORDMARK}} home">{{WORDMARK_INITIAL}}</a>
+ <div class="top-right">
+ <button class="theme-toggle" id="themeToggle" aria-label="Toggle light/dark theme">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4">
+ <circle cx="12" cy="12" r="4"/>
+ <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
+ </svg>
+ </button>
+ <button class="ham" aria-label="Menu (coming soon)">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4">
+ <path d="M3 7h18M3 12h18M3 17h18"/>
+ </svg>
+ </button>
+ </div>
+</header>
+
+<section class="hero" role="img" aria-label="{{HERO_ALT}}"></section>
+
+<main class="meta">
+ <p class="eyebrow">Coming soon</p>
+ <h1 class="wordmark">{{WORDMARK}}</h1>
+ <p class="tagline">{{TAGLINE}}</p>
+
+ <form class="signup" id="signupForm" novalidate>
+ <input type="email" name="email" id="emailInput" placeholder="your@email.com" required autocomplete="email" inputmode="email">
+ <button type="submit" id="signupBtn">Notify me</button>
+ </form>
+ <div class="signup-msg" id="signupMsg" role="status" aria-live="polite"></div>
+</main>
+
+<footer class="gf">
+ <a href="https://agentabrams.com" class="gf-domain">agentabrams.com</a>
+ <span>© {{YEAR}} · {{DOMAIN}}</span>
+</footer>
+
+<script>
+ // Theme toggle
+ (function(){
+ var btn = document.getElementById('themeToggle');
+ btn.addEventListener('click', function(){
+ var cur = document.documentElement.getAttribute('data-theme') || 'light';
+ var next = cur === 'dark' ? 'light' : 'dark';
+ document.documentElement.setAttribute('data-theme', next);
+ try { localStorage.setItem('cs-theme', next); } catch(e){}
+ });
+ })();
+
+ // Email capture
+ (function(){
+ var form = document.getElementById('signupForm');
+ var input = document.getElementById('emailInput');
+ var btn = document.getElementById('signupBtn');
+ var msg = document.getElementById('signupMsg');
+ var ENDPOINT = 'https://agentabrams.com/api/email-signup';
+ var SOURCE = '{{DOMAIN}}';
+
+ function setMsg(text, cls) {
+ msg.textContent = text || '';
+ msg.classList.remove('err', 'ok');
+ if (cls) msg.classList.add(cls);
+ }
+ form.addEventListener('submit', async function(e){
+ e.preventDefault();
+ var email = (input.value || '').trim();
+ if (!email || !/.+@.+\..+/.test(email)) {
+ setMsg('Please enter a valid email.', 'err'); return;
+ }
+ btn.disabled = true; setMsg('');
+ try {
+ var r = await fetch(ENDPOINT, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email: email, source: SOURCE, timestamp: new Date().toISOString() })
+ });
+ if (r.ok) {
+ form.style.display = 'none';
+ setMsg('Thank you. We will be in touch.', 'ok');
+ } else {
+ setMsg('Could not save right now — please try again shortly.', 'err');
+ btn.disabled = false;
+ }
+ } catch (err) {
+ setMsg('Network error — please try again shortly.', 'err');
+ btn.disabled = false;
+ }
+ });
+ })();
+</script>
+</body>
+</html>
(oldest)
·
back to Coming Soon Template
·
snapshot: backup uncommitted work (1 files) 298fd1e →