[object Object]

← back to AsSeenInMovies

card grid: universal hover-preview — wire ::after 3×3 tile-repeat on /movies grid, /people grid, and home rails (movies/people/spotted). --card-bg was already stamped on every grid + rail card; this completes the pattern by adding the hover ::after CSS on .grid-poster, .grid-headshot, .rail-poster, .rail-headshot. Matches wallco-ai pattern (reference: wallco-ai/public/css/site.css:347-385). Skipped hero .poster at line 1267 (movie detail hero) + line 1396 (person detail hero) per non-card rule.

466b16e993dbfbee7636d9209c760320ec5feb67 · 2026-05-28 12:13:16 -0700 · SteveStudio2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 466b16e993dbfbee7636d9209c760320ec5feb67
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Thu May 28 12:13:16 2026 -0700

    card grid: universal hover-preview — wire ::after 3×3 tile-repeat on /movies grid, /people grid, and home rails (movies/people/spotted). --card-bg was already stamped on every grid + rail card; this completes the pattern by adding the hover ::after CSS on .grid-poster, .grid-headshot, .rail-poster, .rail-headshot. Matches wallco-ai pattern (reference: wallco-ai/public/css/site.css:347-385). Skipped hero .poster at line 1267 (movie detail hero) + line 1396 (person detail hero) per non-card rule.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 server.js | 45 ++++++++++++++++++++++++++++++---------------
 1 file changed, 30 insertions(+), 15 deletions(-)

diff --git a/server.js b/server.js
index 66eb933..81ebd99 100644
--- a/server.js
+++ b/server.js
@@ -203,7 +203,7 @@ app.get('/search', async (req, res) => {
   }
   const movieCards = movies.map(m => `
     <a class="grid-card" href="/movie/${m.id}">
-      <div class="grid-poster" style="background-image:url('${esc(m.poster_url || '/img/made-with-ai.svg')}')"></div>
+      <div class="grid-poster" style="background-image:url('${esc(m.poster_url || '/img/made-with-ai.svg')}');--card-bg:url('${esc(m.poster_url || '/img/made-with-ai.svg')}')"></div>
       <div class="grid-body">
         <div class="grid-title">${esc(m.title)}</div>
         <div class="grid-meta">${m.release_year || '—'}</div>
@@ -211,7 +211,7 @@ app.get('/search', async (req, res) => {
     </a>`).join('');
   const peopleCards = people.map(p => `
     <a class="grid-card" href="/person/${p.id}">
-      <div class="grid-headshot" style="background-image:url('${esc(p.headshot_url || '/img/made-with-ai.svg')}')"></div>
+      <div class="grid-headshot" style="background-image:url('${esc(p.headshot_url || '/img/made-with-ai.svg')}');--card-bg:url('${esc(p.headshot_url || '/img/made-with-ai.svg')}')"></div>
       <div class="grid-body">
         <div class="grid-title">${esc(p.full_name)}</div>
         <div class="grid-meta">${(p.primary_profession || []).slice(0,2).join(' · ')}</div>
@@ -226,8 +226,11 @@ app.get('/search', async (req, res) => {
       .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:18px}
       .grid-card{display:block;border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--card);transition:transform .15s;text-decoration:none;color:inherit}
       .grid-card:hover{transform:translateY(-2px);border-color:var(--gold)}
-      .grid-poster{aspect-ratio:2/3;background:#15130f no-repeat center/cover}
-      .grid-headshot{aspect-ratio:3/4;background:#15130f no-repeat center/cover}
+      .grid-poster{aspect-ratio:2/3;background:#15130f no-repeat center/cover;position:relative;overflow:hidden}
+      .grid-headshot{aspect-ratio:3/4;background:#15130f no-repeat center/cover;position:relative;overflow:hidden}
+      .grid-poster::after,.grid-headshot::after{content:"";position:absolute;inset:0;background-image:var(--card-bg,none);background-repeat:repeat;background-size:33.33% 33.33%;background-position:center;opacity:0;transition:opacity 360ms ease;pointer-events:none;will-change:opacity}
+      .grid-card:hover .grid-poster::after,.grid-card:hover .grid-headshot::after,.grid-card:focus-visible .grid-poster::after,.grid-card:focus-visible .grid-headshot::after{opacity:1}
+      @media (prefers-reduced-motion:reduce){.grid-poster::after,.grid-headshot::after{transition:none}}
       .grid-body{padding:10px 12px}
       .grid-title{font-size:13px;line-height:1.3;margin-bottom:4px;color:var(--ink);overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
       .grid-meta{color:var(--ink-faint);font-size:11px;letter-spacing:0.04em}
@@ -476,7 +479,10 @@ app.get('/spotted', async (req, res) => {
         @media (max-width:540px){.top-films .rail{grid-template-columns:repeat(2,1fr)}}
         .top-films .tf-card{display:block;text-decoration:none;color:var(--ink);position:relative;border:1px solid var(--line);border-radius:6px;overflow:hidden;transition:transform .15s,border-color .15s}
         .top-films .tf-card:hover{transform:translateY(-2px);border-color:var(--gold)}
-        .top-films .tf-poster{aspect-ratio:2/3;background:#15130f no-repeat center/cover}
+        .top-films .tf-poster{aspect-ratio:2/3;background:#15130f no-repeat center/cover;position:relative;overflow:hidden}
+        .top-films .tf-poster::after{content:"";position:absolute;inset:0;background-image:var(--card-bg,none);background-repeat:repeat;background-size:33.33% 33.33%;background-position:center;opacity:0;transition:opacity 360ms ease;pointer-events:none;will-change:opacity}
+        .top-films .tf-card:hover .tf-poster::after,.top-films .tf-card:focus-visible .tf-poster::after{opacity:1}
+        @media (prefers-reduced-motion:reduce){.top-films .tf-poster::after{transition:none}}
         .top-films .tf-meta{padding:6px 8px;font-size:11.5px;line-height:1.3;background:rgba(0,0,0,0.4)}
         .top-films .tf-title{display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}
         .top-films .tf-count{display:inline-block;position:absolute;top:6px;right:6px;background:var(--gold);color:#1a1410;font-size:10.5px;letter-spacing:0.08em;font-weight:600;padding:2px 7px;border-radius:999px}
@@ -501,7 +507,7 @@ app.get('/spotted', async (req, res) => {
         <div class="rail">
           ${topMovies.map(m => `
             <a class="tf-card" href="/movie/${m.movie_id}" aria-label="${esc(m.title)}">
-              <div class="tf-poster" style="background-image:url('${esc((m.poster_source||'').toLowerCase()!=='made-with-ai' && m.poster_url ? m.poster_url : '/img/made-with-ai.svg')}')"></div>
+              <div class="tf-poster" style="background-image:url('${esc((m.poster_source||'').toLowerCase()!=='made-with-ai' && m.poster_url ? m.poster_url : '/img/made-with-ai.svg')}');--card-bg:url('${esc((m.poster_source||'').toLowerCase()!=='made-with-ai' && m.poster_url ? m.poster_url : '/img/made-with-ai.svg')}')"></div>
               <span class="tf-count">${m.n}</span>
               <div class="tf-meta"><div class="tf-title">${esc(m.title)}</div><div class="muted" style="font-size:10.5px">${m.release_year || ''}</div></div>
             </a>`).join('')}
@@ -1519,7 +1525,7 @@ app.get('/movies', async (req, res) => {
     const totalPages = Math.max(1, Math.ceil(total / limit));
     const cards = r.rows.map(m => `
       <a class="grid-card" href="/movie/${m.id}">
-        <div class="grid-poster" style="background-image:url('${esc(m.poster_url || '')}')"></div>
+        <div class="grid-poster" style="background-image:url('${esc(m.poster_url || '')}');--card-bg:url('${esc(m.poster_url || '')}')"></div>
         <div class="grid-body">
           <div class="grid-title">${esc(m.title)}</div>
           <div class="grid-meta">${m.release_year || '—'}${m.vote_average ? ' · ★ ' + Number(m.vote_average).toFixed(1) : ''}</div>
@@ -1572,7 +1578,10 @@ app.get('/movies', async (req, res) => {
         .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--movies-card),1fr));gap:20px}
         .grid-card{display:block;border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--card);transition:transform .15s}
         .grid-card:hover{transform:translateY(-2px)}
-        .grid-poster{aspect-ratio:2/3;background:#15130f no-repeat center/cover}
+        .grid-poster{aspect-ratio:2/3;background:#15130f no-repeat center/cover;position:relative;overflow:hidden}
+        .grid-poster::after{content:"";position:absolute;inset:0;background-image:var(--card-bg,none);background-repeat:repeat;background-size:33.33% 33.33%;background-position:center;opacity:0;transition:opacity 360ms ease;pointer-events:none;will-change:opacity}
+        .grid-card:hover .grid-poster::after,.grid-card:focus-visible .grid-poster::after{opacity:1}
+        @media (prefers-reduced-motion:reduce){.grid-poster::after{transition:none}}
         .grid-body{padding:10px 12px}
         .grid-title{font-size:13.5px;line-height:1.35;margin-bottom:4px;color:var(--ink);overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
         .grid-meta{color:var(--ink-faint);font-size:11px;letter-spacing:0.04em}
@@ -1639,7 +1648,7 @@ app.get('/people', async (req, res) => {
     const totalPages = Math.max(1, Math.ceil(total / limit));
     const cards = r.rows.map(p => `
       <a class="grid-card person" href="/person/${p.id}">
-        <div class="grid-headshot" style="background-image:url('${esc(p.headshot_url || '')}')"></div>
+        <div class="grid-headshot" style="background-image:url('${esc(p.headshot_url || '')}');--card-bg:url('${esc(p.headshot_url || '')}')"></div>
         <div class="grid-body">
           <div class="grid-title">${esc(p.full_name)}</div>
           <div class="grid-meta">${(p.primary_profession || []).slice(0,2).join(' · ')}${p.birth_year ? ' · ' + p.birth_year + (p.death_year ? '–' + p.death_year : '') : ''}</div>
@@ -1670,7 +1679,10 @@ app.get('/people', async (req, res) => {
         .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--people-card),1fr));gap:18px}
         .grid-card{display:block;border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--card);transition:transform .15s}
         .grid-card:hover{transform:translateY(-2px)}
-        .grid-headshot{aspect-ratio:3/4;background:#15130f no-repeat center/cover}
+        .grid-headshot{aspect-ratio:3/4;background:#15130f no-repeat center/cover;position:relative;overflow:hidden}
+        .grid-headshot::after{content:"";position:absolute;inset:0;background-image:var(--card-bg,none);background-repeat:repeat;background-size:33.33% 33.33%;background-position:center;opacity:0;transition:opacity 360ms ease;pointer-events:none;will-change:opacity}
+        .grid-card:hover .grid-headshot::after,.grid-card:focus-visible .grid-headshot::after{opacity:1}
+        @media (prefers-reduced-motion:reduce){.grid-headshot::after{transition:none}}
         .grid-body{padding:10px 12px}
         .grid-title{font-size:13.5px;line-height:1.35;margin-bottom:4px;color:var(--ink)}
         .grid-meta{color:var(--ink-faint);font-size:11px;letter-spacing:0.04em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -1788,19 +1800,19 @@ app.get('/', async (_req, res) => {
   }
   const movieCards = featuredMovies.map(m => `
     <a class="rail-card" href="/movie/${m.id}">
-      <div class="rail-poster" style="background-image:url('${esc(m.poster_url || '')}')"></div>
+      <div class="rail-poster" style="background-image:url('${esc(m.poster_url || '')}');--card-bg:url('${esc(m.poster_url || '')}')"></div>
       <div class="rail-title">${esc(m.title)}</div>
       <div class="rail-meta">${m.release_year || '—'}</div>
     </a>`).join('');
   const peopleCards = featuredPeople.map(p => `
     <a class="rail-card" href="/person/${p.id}">
-      <div class="rail-headshot" style="background-image:url('${esc(p.headshot_url || '')}')"></div>
+      <div class="rail-headshot" style="background-image:url('${esc(p.headshot_url || '')}');--card-bg:url('${esc(p.headshot_url || '')}')"></div>
       <div class="rail-title">${esc(p.full_name)}</div>
       <div class="rail-meta">${(p.primary_profession || []).slice(0,2).join(' · ')}</div>
     </a>`).join('');
   const spottedCards = featuredSpotted.map(s => `
     <a class="rail-card" href="/movie/${s.movie_id}#spotted">
-      <div class="rail-poster" style="background-image:url('${esc((s.poster_source||'').toLowerCase()!=='made-with-ai' && s.poster_url ? s.poster_url : '/img/made-with-ai.svg')}')"></div>
+      <div class="rail-poster" style="background-image:url('${esc((s.poster_source||'').toLowerCase()!=='made-with-ai' && s.poster_url ? s.poster_url : '/img/made-with-ai.svg')}');--card-bg:url('${esc((s.poster_source||'').toLowerCase()!=='made-with-ai' && s.poster_url ? s.poster_url : '/img/made-with-ai.svg')}')"></div>
       <div class="rail-title">${esc(s.product_name)}${s.brand ? ' · <span style="color:var(--muted)">'+esc(s.brand)+'</span>' : ''}</div>
       <div class="rail-meta">${esc(s.title)}${s.release_year ? ' ('+s.release_year+')' : ''}</div>
     </a>`).join('');
@@ -1848,8 +1860,11 @@ ${process.env.GA_ID ? `<script async src="https://www.googletagmanager.com/gtag/
   @media (max-width:540px) { .rail { grid-template-columns:repeat(2,1fr) } }
   .rail-card { display:block; text-decoration:none; color:var(--ink); border:1px solid var(--line); border-radius:8px; overflow:hidden; background:rgba(201,161,75,0.03); transition:transform .15s }
   .rail-card:hover { transform:translateY(-3px); border-color:var(--gold) }
-  .rail-poster { aspect-ratio:2/3; background:#15130f no-repeat center/cover }
-  .rail-headshot { aspect-ratio:3/4; background:#15130f no-repeat center/cover }
+  .rail-poster { aspect-ratio:2/3; background:#15130f no-repeat center/cover; position:relative; overflow:hidden }
+  .rail-headshot { aspect-ratio:3/4; background:#15130f no-repeat center/cover; position:relative; overflow:hidden }
+  .rail-poster::after,.rail-headshot::after{content:"";position:absolute;inset:0;background-image:var(--card-bg,none);background-repeat:repeat;background-size:33.33% 33.33%;background-position:center;opacity:0;transition:opacity 360ms ease;pointer-events:none;will-change:opacity}
+  .rail-card:hover .rail-poster::after,.rail-card:hover .rail-headshot::after,.rail-card:focus-visible .rail-poster::after,.rail-card:focus-visible .rail-headshot::after{opacity:1}
+  @media (prefers-reduced-motion:reduce){.rail-poster::after,.rail-headshot::after{transition:none}}
   .rail-title { padding:8px 10px 2px; font-size:12.5px; line-height:1.3; overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical }
   .rail-meta { padding:0 10px 8px; color:var(--muted); font-size:10.5px; letter-spacing:0.06em }
   .blurb-grid { display:grid; grid-template-columns:1fr 1fr; gap:24px; margin-top:24px }

← 694e783 Add rel=noreferrer to every target=_blank link  ·  back to AsSeenInMovies  ·  Add per-site favicon (kills /favicon.ico 404) 11799d1 →