← back to CelebritySignatures
Celeb Signatures: slow 16s ease-in-out float on hero logo (premium/calm, reduced-motion guarded)
b10be7bb8f6c7b9c57a367ba8ec677c4a61ed30b · 2026-08-03 14:28:01 -0700 · Steve Abrams
Files touched
Diff
commit b10be7bb8f6c7b9c57a367ba8ec677c4a61ed30b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 3 14:28:01 2026 -0700
Celeb Signatures: slow 16s ease-in-out float on hero logo (premium/calm, reduced-motion guarded)
---
public/index.html | 25 ++++++++++++++++++-------
1 file changed, 18 insertions(+), 7 deletions(-)
diff --git a/public/index.html b/public/index.html
index 71d651a..c774212 100644
--- a/public/index.html
+++ b/public/index.html
@@ -72,6 +72,11 @@
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); }
+ .chip-face { width:16px; height:16px; border-radius:50%; object-fit:cover; vertical-align:-3px; margin-right:5px; }
+ .ap-portrait { width:56px; height:56px; border-radius:50%; object-fit:cover; border:2px solid var(--line); margin-right:14px; vertical-align:middle; }
+ .ap-figure { text-align:center; padding:6px 0 2px; }
+ .ap-figure img { max-width:340px; max-height:420px; width:100%; object-fit:contain; border-radius:8px; border:1px solid var(--line); }
+ .ap-figure .ap-sub { margin-top:6px; }
/* 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; } }
/* ── 6 shopping layouts — signature only; ALL info reveals on hover ──
@@ -141,7 +146,9 @@
.hero { position:relative; overflow:hidden; border-bottom:1px solid var(--line); background:#fbfaf7; }
.hero-bg { position:absolute; inset:-2%; background:url('/output/collage-masters-of-the-met.png') center 30%/cover no-repeat; opacity:.16; filter:saturate(.7); }
.hero-inner { position:relative; max-width:860px; margin:0 auto; padding:64px 24px 58px; text-align:center; }
- .hero-logo { max-width:min(560px,86%); mix-blend-mode:multiply; }
+ .hero-logo { max-width:min(560px,86%); mix-blend-mode:multiply; animation:heroLogoFloat 16s ease-in-out infinite; will-change:transform; }
+ @keyframes heroLogoFloat { 0%{transform:translateY(0) scale(1)} 50%{transform:translateY(-12px) scale(1.015)} 100%{transform:translateY(0) scale(1)} }
+ @media (prefers-reduced-motion:reduce){ .hero-logo{ animation:none } }
.hero-tag { font:italic 400 18px/1.6 'Playfair Display', Georgia, serif; color:#4a463f; max-width:560px; margin:14px auto 22px; }
.hero-ctas { display:flex; gap:12px; justify-content:center; flex-wrap:wrap; }
.hbtn { padding:11px 26px; border-radius:999px; background:var(--ink); color:#fff; text-decoration:none; font-size:14.5px; border:1px solid var(--ink); }
@@ -398,7 +405,7 @@ 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} signatures`:''}</a>
+ <a class="info-chip" data-qid="${qid}" href="?artist=${qid}">${PORTRAITS[qid]?`<img class="chip-face" loading="lazy" referrerpolicy="no-referrer" src="${esc(PORTRAITS[qid])}" alt="" onerror="this.remove()">`:''}Details${evoN>1?` · ${evoN} signatures`:''}</a>
</div></div>`;
}).join('');
}
@@ -477,7 +484,7 @@ document.querySelector('.tabs').onclick = (e) => { const v=e.target.dataset.v; i
// ── artist details popup: all metadata + signature evolution + works from the
// Met Open Access API (free, PD images, CORS-open) loaded on demand ──
-let EVO = {}, BY_QID = {}, WORKS_CACHE = {};
+let EVO = {}, BY_QID = {}, WORKS_CACHE = {}, PORTRAITS = {};
function evoStripHTML(qid){
const e=EVO[qid];
if(!e||!e.sigs||e.sigs.length<2) return '';
@@ -548,8 +555,10 @@ function openArtist(qid, push=true){
const ub=r.usable_in_commercial_collage;
const uClass=ub==='yes'?'use-yes':ub==='permission-needed'?'use-perm':'use-review';
const uLabel=ub==='yes'?'Free to use':ub==='permission-needed'?'Permission needed':'Verify first';
+ const isArtist = r.category==='Artists';
+ const portrait = PORTRAITS[qid];
$('#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-sig">${portrait?`<img class="ap-portrait" referrerpolicy="no-referrer" src="${esc(portrait)}" alt="Portrait of ${esc(r.full_name)}" onerror="this.remove()">`:''}<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'} · ${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>
@@ -557,8 +566,9 @@ function openArtist(qid, push=true){
<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>
+ ${isArtist
+ ? `<div class="ap-sec">Works — open museum collections</div><div class="works" id="apWorks"><span class="ap-sub">loading works…</span></div>`
+ : (portrait ? `<div class="ap-sec">Portrait</div><div class="ap-figure"><img referrerpolicy="no-referrer" src="${esc(portrait.replace('width=120','width=520'))}" alt="Portrait of ${esc(r.full_name)}" onerror="this.closest('.ap-figure').remove()"><div class="ap-sub">Public-domain / CC portrait via Wikimedia Commons</div></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');
@@ -569,7 +579,7 @@ function openArtist(qid, push=true){
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=>{
+ if (isArtist) loadWorks(qid, r.full_name).then(ws=>{
const el=$('#apWorks'); if(!el) return;
el.innerHTML = ws.length ? ws.map(w=>`<a href="${esc(w.url)}" target="_blank" rel="noopener noreferrer"><img loading="lazy" referrerpolicy="no-referrer" src="${esc(w.img)}" alt="${esc(w.title)}" onerror="this.closest('a').remove()"><span class="wt">${esc(w.title)}${w.date?' · '+esc(w.date):''}${w.src?' · '+esc(w.src):''}</span></a>`).join('')
: `<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>`;
@@ -592,6 +602,7 @@ function maybeOpenFromURL(){ const q=new URLSearchParams(location.search).get('a
fetch('/api/signatures').then(r=>r.json()).then(d => { DATA=d; readURL(); buildChips(); render(); setView(prefs.view); maybeOpenFromURL(); });
fetch('/api/signature-evolution').then(r=>r.ok?r.json():{}).then(e=>{ EVO=e||{}; if(DATA.length) render(); }).catch(()=>{});
+fetch('/api/portraits').then(r=>r.ok?r.json():{}).then(p=>{ PORTRAITS=p||{}; if(DATA.length) render(); }).catch(()=>{});
addEventListener('popstate', maybeOpenFromURL);
</script>
</body>
← 4562ef6 auto-save: 2026-08-03T14:24:54 (6 files) — data/portraits.js
·
back to CelebritySignatures
·
Portraits for non-artists: fetch-portraits.mjs pulls Wikidat 9913d83 →