← back to CelebritySignatures
Five-Horsemen UX pass (ui-ux + graphic-designer panel verdicts applied): global nav masthead + Playfair wordmark, name SEARCH on the grid, value-prop hero line, category chip counts, consumer language (Free to use / Permission needed), document-style cards + hover-reveal details chip, popup recomposition (full-width signature, Playfair name, sticky Order-a-mural CTA), dialog a11y (role/aria-modal/focus return), upload page $10 stat callout (TK-10181)
03c2beba41a8f60e4123ad2e0b2a1b66929dc794 · 2026-08-03 11:52:28 -0700 · Steve Abrams
Files touched
M public/index.htmlM public/upload-signature.html
Diff
commit 03c2beba41a8f60e4123ad2e0b2a1b66929dc794
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 3 11:52:28 2026 -0700
Five-Horsemen UX pass (ui-ux + graphic-designer panel verdicts applied): global nav masthead + Playfair wordmark, name SEARCH on the grid, value-prop hero line, category chip counts, consumer language (Free to use / Permission needed), document-style cards + hover-reveal details chip, popup recomposition (full-width signature, Playfair name, sticky Order-a-mural CTA), dialog a11y (role/aria-modal/focus return), upload page $10 stat callout (TK-10181)
---
public/index.html | 152 +++++++++++++++++++++++++++++++++----------
public/upload-signature.html | 9 ++-
2 files changed, 126 insertions(+), 35 deletions(-)
diff --git a/public/index.html b/public/index.html
index 5eaa4b2..7cdb5b6 100644
--- a/public/index.html
+++ b/public/index.html
@@ -3,14 +3,29 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Celebrity Signatures</title>
+<title>Celebrity Signatures — authentic signatures of history's icons</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400;1,600&family=Dancing+Script:wght@600&display=swap" rel="stylesheet">
<style>
:root { --cols: 5; --ink:#1a1a1a; --muted:#6b6b6b; --line:#e6e3dc; --bg:#f7f5f0; --card:#fff; }
* { box-sizing: border-box; }
body { margin:0; font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; color:var(--ink); background:var(--bg); }
- header { padding:26px 28px 14px; border-bottom:1px solid var(--line); background:#fff; position:sticky; top:0; z-index:20; }
- h1 { margin:0; font-weight:600; font-size:22px; letter-spacing:.01em; }
- h1 span { color:var(--muted); font-weight:400; font-size:14px; margin-left:8px; }
+ /* masthead: Playfair wordmark + global nav across the three surfaces */
+ .masthead { position:sticky; top:0; z-index:30; display:flex; align-items:center; gap:22px; height:56px; padding:0 24px; background:#fff; border-bottom:1px solid var(--line); }
+ .wordmark { font:italic 600 24px/1 'Playfair Display', Georgia, serif; color:var(--ink); text-decoration:none; letter-spacing:.01em; }
+ .gnav { margin-left:auto; display:flex; gap:20px; }
+ .gnav a { font-size:13.5px; color:var(--muted); text-decoration:none; padding:4px 2px; border-bottom:2px solid transparent; }
+ .gnav a.on, .gnav a:hover { color:var(--ink); border-bottom-color:var(--ink); }
+ header { padding:18px 28px 14px; border-bottom:1px solid var(--line); background:#fff; }
+ h1 { margin:0; font:700 26px/1.2 'Playfair Display', Georgia, serif; letter-spacing:.01em; }
+ h1 span { color:var(--muted); font:400 14px -apple-system,BlinkMacSystemFont,sans-serif; margin-left:8px; }
+ .valueprop { margin:6px 0 0; color:var(--muted); font-size:14px; }
+ .valueprop a { color:var(--green, #1d7a36); }
+ .search { margin-top:14px; }
+ .search input { width:min(420px,100%); padding:9px 14px; border:1px solid var(--line); border-radius:999px; font:inherit; background:#fffef9; }
+ .chip-count { background:rgba(0,0,0,.07); border-radius:9px; padding:0 6px; font-size:11px; margin-left:5px; }
+ .chip.on .chip-count { background:rgba(255,255,255,.25); }
.controls { display:flex; flex-wrap:wrap; gap:18px 22px; align-items:center; margin-top:14px; }
.controls label { font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin-right:8px; }
select, input[type=range] { font:inherit; }
@@ -21,7 +36,7 @@
.density { display:flex; align-items:center; gap:10px; }
main { padding:22px 28px 60px; }
.grid { display:grid; grid-template-columns:repeat(var(--cols), 1fr); gap:18px; }
- .card { background:var(--card); border:1px solid var(--line); border-radius:12px; overflow:hidden; display:flex; flex-direction:column; }
+ .card { background:#fffef9; border:1px solid #ece8df; border-radius:4px; overflow:hidden; display:flex; flex-direction:column; box-shadow:0 1px 3px rgba(0,0,0,.06), 0 4px 16px rgba(0,0,0,.04); }
.sig { height:120px; display:flex; align-items:center; justify-content:center; padding:16px; background:#fff; border-bottom:1px solid var(--line); }
.sig img { max-width:100%; max-height:100%; object-fit:contain; }
.meta { padding:11px 13px 13px; display:flex; flex-direction:column; gap:6px; flex:1; }
@@ -50,19 +65,29 @@
.evo-cell { text-align:center; }
.evo-cell img { max-height:44px; max-width:140px; object-fit:contain; background:#fff; border:1px solid var(--line); border-radius:6px; padding:3px; }
.evo-cell .y { font-size:10px; color:var(--muted); }
- /* minimal card: details chip */
- .info-chip { display:inline-block; margin-top:2px; font-size:11px; color:var(--muted); border:1px solid var(--line); border-radius:999px; padding:2px 9px; cursor:pointer; text-decoration:none; }
+ /* minimal card: details chip — hidden on load; revealed only when the card /
+ signature is hovered (or keyboard-focused). Hover the signature to surface it,
+ then click to open the details popup. */
+ .info-chip { display:inline-block; margin-top:2px; font-size:11px; color:var(--muted); border:1px solid var(--line); border-radius:999px; padding:2px 9px; cursor:pointer; text-decoration:none;
+ opacity:0; transform:translateY(2px); pointer-events:none; transition:opacity .16s ease, transform .16s ease; }
+ .card:hover .info-chip, .card:focus-within .info-chip, .info-chip:focus-visible { opacity:1; transform:none; pointer-events:auto; }
.info-chip:hover { background:var(--ink); color:#fff; border-color:var(--ink); }
+ /* touch / no-hover devices can't reveal on hover — keep the chip visible there */
+ @media (hover: none) { .info-chip { opacity:1; transform:none; pointer-events:auto; } }
/* artist details popup */
#artistPop { position:fixed; inset:0; z-index:50; display:flex; align-items:center; justify-content:center; }
#artistPop[hidden] { display:none; }
.ap-back { position:absolute; inset:0; background:rgba(20,18,14,.55); }
- .ap-card { position:relative; background:#fff; border-radius:16px; max-width:820px; width:min(94vw,820px); max-height:88vh; overflow:auto; padding:26px 28px; box-shadow:0 24px 80px rgba(0,0,0,.35); }
- .ap-x { position:absolute; top:12px; right:14px; border:none; background:none; font-size:26px; line-height:1; cursor:pointer; color:var(--muted); }
+ .ap-card { position:relative; background:#fff; border-radius:10px; max-width:820px; width:min(94vw,820px); max-height:88vh; overflow:auto; padding:0 0 0; box-shadow:0 24px 80px rgba(0,0,0,.35); }
+ .ap-inner { padding:22px 28px 84px; }
+ .ap-x { position:absolute; top:12px; right:14px; border:none; background:none; font-size:26px; line-height:1; cursor:pointer; color:var(--muted); z-index:2; }
.ap-x:hover { color:var(--ink); }
- .ap-sig { text-align:center; padding:8px 0 2px; }
- .ap-sig img { max-height:90px; max-width:80%; object-fit:contain; }
- .ap-name { font-size:22px; font-weight:650; margin:6px 0 2px; }
+ .ap-sig { text-align:center; padding:16px 0 14px; border-bottom:1px solid var(--line); margin-bottom:12px; }
+ .ap-sig img { max-height:110px; max-width:92%; object-fit:contain; }
+ .ap-name { font:400 28px/1.15 'Playfair Display', Georgia, serif; margin:6px 0 2px; }
+ .ap-cta { position:sticky; bottom:0; background:#fff; border-top:1px solid var(--line); padding:12px 28px; text-align:center; }
+ .ap-cta a { display:inline-block; padding:10px 26px; border-radius:999px; background:var(--ink); color:#fff; text-decoration:none; font-size:14.5px; }
+ .ap-cta a:hover { background:#000; }
.ap-life { color:var(--muted); font-size:13px; margin-bottom:10px; }
.ap-rowline { display:flex; flex-wrap:wrap; gap:6px; margin:8px 0; }
.ap-sec { font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin:16px 0 6px; }
@@ -74,30 +99,60 @@
</style>
</head>
<body>
+<div class="masthead">
+ <a class="wordmark" href="/">Celebrity Signatures</a>
+ <nav class="gnav">
+ <a href="/" class="on">Browse signatures</a>
+ <a href="/murals">Order a mural</a>
+ <a href="/upload">Upload your signature</a>
+ </nav>
+</div>
<header>
- <h1>Celebrity Signatures <span id="count"></span></h1>
+ <h1>Authentic signatures of history's icons <span id="count"></span></h1>
+ <p class="valueprop">1,300+ verified signatures — artists, presidents, founders — each traced to a real archival source. <a href="/murals">Order any collection as a made-to-order wall mural →</a></p>
<div class="tabs">
<span class="tab on" data-v="grid">Grid</span>
<span class="tab" data-v="collages">Collages</span>
- <a class="tab" href="/upload" style="text-decoration:none">✍ Upload yours</a>
</div>
<div class="controls" id="controls">
+ <div class="search"><input type="search" id="q" placeholder="Search any name — Monet, Lincoln, Kahlo…" aria-label="Search signatures by name"></div>
<div><label>Category</label></div>
<div class="chips" id="catChips"></div>
<div><label>Use</label>
<select id="useFilter">
- <option value="all">All</option>
- <option value="yes">Collage-usable only</option>
- <option value="permission-needed">Permission-needed only</option>
+ <option value="all">All signatures</option>
+ <option value="yes">Free to use</option>
+ <option value="permission-needed">Permission needed</option>
</select>
</div>
<div><label>Sort</label>
<select id="sort">
- <option value="cat">Category, then rank</option>
- <option value="name">Name A→Z</option>
- <option value="notability">Notability ↓</option>
- <option value="usable">Usable first</option>
- <option value="risk">Risk low→high</option>
+ <optgroup label="Category">
+ <option value="cat">Category, then rank</option>
+ </optgroup>
+ <optgroup label="Name">
+ <option value="name">Name A→Z</option>
+ <option value="name-desc">Name Z→A</option>
+ </optgroup>
+ <optgroup label="Rank / notability">
+ <option value="rank">Rank (1 → n)</option>
+ <option value="notability">Notability ↓</option>
+ </optgroup>
+ <optgroup label="Usage & risk">
+ <option value="usable">Usable first</option>
+ <option value="risk">Risk low → high</option>
+ <option value="risk-desc">Risk high → low</option>
+ <option value="license">License A→Z</option>
+ </optgroup>
+ <optgroup label="Life">
+ <option value="living">Living first</option>
+ <option value="deceased">Deceased first</option>
+ <option value="death">Death date (earliest → latest)</option>
+ </optgroup>
+ <optgroup label="Holdings & signatures">
+ <option value="museums">Museums held (most first)</option>
+ <option value="sigcount">Signatures over time (most first)</option>
+ </optgroup>
</select>
</div>
<div class="density"><label>Density</label>
@@ -121,6 +176,11 @@ let DATA = [];
const $ = (s) => document.querySelector(s);
const notability = (r) => { const m=(r.reason_for_ranking||'').match(/(\d+) language/); return m?+m[1]:0; };
const riskRank = { low:0, medium:1, high:2 };
+// field extractors used by the "sort by any field" comparators
+const qidOf = (r) => (r.wikidata||'').split('/').pop();
+const evoCount = (r) => (EVO[qidOf(r)]?.sigs?.length) || 0;
+const museumCount = (r) => Array.isArray(r.museums) ? r.museums.length : 0;
+const deathYear = (r) => { const m=(r.death_date||'').match(/-?\d{3,4}/); return m ? +m[0] : Infinity; }; // undated / living sort last
function save() { localStorage.setItem(LS, JSON.stringify(prefs)); }
// ── href-drill: filter state (category / usability) is URL-addressable (shareable + deep-link + back-button) ──
@@ -136,15 +196,26 @@ function render() {
$('#useFilter').value = prefs.use;
$('#sort').value = prefs.sort;
+ const q = (prefs.q||'').toLowerCase().trim();
let rows = DATA.filter(r => (prefs.cat==='All' || r.category===prefs.cat)
- && (prefs.use==='all' || r.usable_in_commercial_collage===prefs.use));
+ && (prefs.use==='all' || r.usable_in_commercial_collage===prefs.use)
+ && (!q || r.full_name.toLowerCase().includes(q)));
const cats = ['Oldest Signatures','Declaration of Independence','Politics','Artists'];
const sorters = {
cat: (a,b) => cats.indexOf(a.category)-cats.indexOf(b.category) || a.rank-b.rank,
name: (a,b) => a.full_name.localeCompare(b.full_name),
- notability: (a,b) => notability(b)-notability(a),
+ 'name-desc': (a,b) => b.full_name.localeCompare(a.full_name),
+ rank: (a,b) => a.rank-b.rank,
+ notability: (a,b) => notability(b)-notability(a) || a.rank-b.rank,
usable: (a,b) => (a.usable_in_commercial_collage==='yes'?0:1)-(b.usable_in_commercial_collage==='yes'?0:1) || a.rank-b.rank,
risk: (a,b) => (riskRank[a.risk_level]??9)-(riskRank[b.risk_level]??9) || a.rank-b.rank,
+ 'risk-desc': (a,b) => (riskRank[b.risk_level]??-1)-(riskRank[a.risk_level]??-1) || a.rank-b.rank,
+ license: (a,b) => (a.image_license||'~').localeCompare(b.image_license||'~') || a.rank-b.rank,
+ living: (a,b) => (a.deceased==='yes'?1:0)-(b.deceased==='yes'?1:0) || a.rank-b.rank,
+ deceased: (a,b) => (a.deceased==='yes'?0:1)-(b.deceased==='yes'?0:1) || a.rank-b.rank,
+ death: (a,b) => deathYear(a)-deathYear(b) || a.rank-b.rank,
+ museums: (a,b) => museumCount(b)-museumCount(a) || a.rank-b.rank,
+ sigcount: (a,b) => evoCount(b)-evoCount(a) || a.rank-b.rank,
};
rows.sort(sorters[prefs.sort] || sorters.cat);
@@ -160,18 +231,23 @@ function render() {
<div class="sig"><img loading="lazy" src="${r.signature_image_url}" alt="Signature of ${r.full_name}" onerror="this.parentNode.innerHTML='<span style=color:#bbb;font-size:12px>image unavailable</span>'"></div>
<div class="meta">
<div class="name">${r.full_name}</div>
- <a class="info-chip" data-qid="${qid}" href="?artist=${qid}">ⓘ details${evoN>1?` · ✍ ${evoN}`:''}</a>
+ <a class="info-chip" data-qid="${qid}" href="?artist=${qid}">Details${evoN>1?` · ${evoN} signatures`:''}</a>
</div></div>`;
}).join('');
}
function buildChips() {
const cats = ['All','Oldest Signatures','Declaration of Independence','Politics','Artists'];
- $('#catChips').innerHTML = cats.map(c => `<span class="chip${c===prefs.cat?' on':''}" data-c="${c}">${c}</span>`).join('');
- $('#catChips').onclick = (e) => { const c=e.target.dataset.c; if(!c) return; prefs.cat=c; save(); writeURL(true);
+ const counts = {}; DATA.forEach(r => { counts[r.category]=(counts[r.category]||0)+1; });
+ counts['All'] = DATA.length;
+ $('#catChips').innerHTML = cats.map(c => `<span class="chip${c===prefs.cat?' on':''}" data-c="${c}">${c}<span class="chip-count">${counts[c]||0}</span></span>`).join('');
+ $('#catChips').onclick = (e) => { const chip=e.target.closest('.chip'); const c=chip&&chip.dataset.c; if(!c) return; prefs.cat=c; save(); writeURL(true);
document.querySelectorAll('.chip').forEach(x=>x.classList.toggle('on', x.dataset.c===c)); render(); };
}
$('#density').oninput = (e) => { prefs.density=+e.target.value; save(); render(); };
+// name search — debounced, transient (not persisted)
+let qTimer=null;
+$('#q').oninput = (e) => { clearTimeout(qTimer); qTimer=setTimeout(()=>{ prefs.q=e.target.value; render(); },150); };
$('#useFilter').onchange = (e) => { prefs.use=e.target.value; save(); writeURL(true); render(); };
// a card data point (category / usability) is an href-drill atom → filter in place + reflect into the URL
document.addEventListener('click', (e) => { const d=e.target.closest('a.drill[data-filter]'); if(!d) return; if(e.metaKey||e.ctrlKey||e.shiftKey||e.button===1) return; e.preventDefault(); e.stopPropagation();
@@ -284,18 +360,26 @@ function openArtist(qid, push=true){
if (!r) return;
const ub=r.usable_in_commercial_collage;
const uClass=ub==='yes'?'use-yes':ub==='permission-needed'?'use-perm':'use-review';
- $('#apBody').innerHTML = `
+ const uLabel=ub==='yes'?'Free to use':ub==='permission-needed'?'Permission needed':'Verify first';
+ $('#apBody').innerHTML = `<div class="ap-inner">
<div class="ap-sig"><img src="${r.signature_image_url}" alt="Signature of ${esc(r.full_name)}"></div>
<div class="ap-name">${esc(r.full_name)}</div>
- <div class="ap-life">${r.deceased==='yes'?'† '+esc(r.death_date||''):'● living'} · rank #${r.rank} · ${esc((r.reason_for_ranking||'').replace('Cross-wiki notability: ',''))}</div>
- <div class="ap-rowline"><span class="tag">${drill('cat',r.category)}</span><span class="badge b-${r.risk_level}">${r.risk_level}</span><span class="badge ${uClass}">${drill('use',ub,ub==='yes'?'usable':ub)}</span></div>
- ${Array.isArray(r.museums)&&r.museums.length?`<div class="ap-sec">🏛 In the collections of</div><div class="ap-sub">${esc(r.museums.join(' · '))}</div>`:''}
+ <div class="ap-life">${r.deceased==='yes'?'† '+esc(r.death_date||''):'● living'} · ${esc((r.reason_for_ranking||'').replace('Cross-wiki notability: ',''))}</div>
+ <div class="ap-rowline"><span class="tag">${drill('cat',r.category)}</span><span class="badge ${uClass}">${drill('use',ub,uLabel)}</span></div>
+ ${Array.isArray(r.museums)&&r.museums.length?`<div class="ap-sec">In the collections of</div><div class="ap-sub">${esc(r.museums.join(' · '))}</div>`:''}
<div class="ap-sec">License & sources</div>
<div class="ap-sub">${esc(r.image_license||'')} · <a href="${esc(r.wikidata)}" target="_blank" rel="noopener noreferrer">Wikidata</a> · <a href="${esc(r.backup_source)}" target="_blank" rel="noopener noreferrer">signature source</a></div>
${evoStripHTML(qid)}
<div class="ap-sec">Works — open museum collections</div>
- <div class="works" id="apWorks"><span class="ap-sub">loading works…</span></div>`;
- $('#artistPop').hidden = false;
+ <div class="works" id="apWorks"><span class="ap-sub">loading works…</span></div>
+ </div>
+ <div class="ap-cta"><a href="/murals?name=${encodeURIComponent(r.full_name)}">Order a mural featuring this signature →</a></div>`;
+ const pop=$('#artistPop');
+ pop.hidden = false;
+ pop.setAttribute('role','dialog'); pop.setAttribute('aria-modal','true');
+ pop.setAttribute('aria-label', r.full_name + ' — signature details');
+ POP_RETURN_FOCUS = document.activeElement;
+ document.querySelector('.ap-x').focus();
document.body.style.overflow = 'hidden';
if (push) { const u=new URLSearchParams(location.search); u.set('artist',qid); history.pushState({},'', '?'+u.toString()); }
loadWorks(qid, r.full_name).then(ws=>{
@@ -304,10 +388,12 @@ function openArtist(qid, push=true){
: `<span class="ap-sub">no openly-licensed works found in the open museum APIs — <a href="${esc(r.wikidata)}" target="_blank" rel="noopener noreferrer">see Wikidata</a></span>`;
});
}
+let POP_RETURN_FOCUS = null;
function closeArtist(push=true){
if ($('#artistPop').hidden) return;
$('#artistPop').hidden = true;
document.body.style.overflow = '';
+ if (POP_RETURN_FOCUS && POP_RETURN_FOCUS.focus) { POP_RETURN_FOCUS.focus(); POP_RETURN_FOCUS = null; }
if (push) { const u=new URLSearchParams(location.search); u.delete('artist'); const q=u.toString(); history.pushState({},'', q?('?'+q):location.pathname); }
}
document.addEventListener('click', (e)=>{
diff --git a/public/upload-signature.html b/public/upload-signature.html
index 969b5fe..64322b2 100644
--- a/public/upload-signature.html
+++ b/public/upload-signature.html
@@ -42,10 +42,15 @@
<div class="card">
<h2>Your signature. Your account. Your commission.</h2>
+ <div style="display:flex;gap:26px;align-items:center;margin:14px 0 10px">
+ <div><div style="font-size:40px;font-weight:700;color:var(--green);line-height:1">$10</div>
+ <div class="sub">earned by you, per download</div></div>
+ <div><div style="font-size:40px;font-weight:700;line-height:1">50%</div>
+ <div class="sub">your cut of every $20 download</div></div>
+ </div>
<div class="sub">Upload your own signature as a digital file. It is tied to your account and stored privately —
the file is <b>never shared, resold, or publicly exposed</b>. Every single download is tracked to your account
- and earns you a commission (currently <b>50% of a $20 download</b>). Payouts are reconciled from the
- download ledger.</div>
+ and earns you a commission. Payouts are reconciled from the download ledger.</div>
</div>
<div class="card" id="authCard">
← b5a9458 Celebrity signature upload + commission system: account-owne
·
back to CelebritySignatures
·
Custom mural sizing: user enters mural width/height (ft) and 46fd5bd →