[object Object]

← 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

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 →