← back to Paul Conrad Cartoons Rebrand
scripts/artifact-template.html
379 lines
<title>Conrad Cartoon Index</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@300;500;700&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,600;1,6..72,400&display=swap');
:root {
--paper: #eceae1;
--card: #f5f3ec;
--ink: #17150f;
--ink-soft: #57534a;
--line: #d5d1c5;
--red: #a11e12;
--gold: #8a6d24;
--doc: #2f5d43;
--arc: #7a5a1e;
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--paper: #16140e;
--card: #201d15;
--ink: #ece7d8;
--ink-soft: #a49d8b;
--line: #332f25;
--red: #e0694e;
--gold: #d0b25f;
--doc: #6fbf8f;
--arc: #d9b25f;
color-scheme: dark;
}
}
:root[data-theme="dark"] {
--paper: #16140e;
--card: #201d15;
--ink: #ece7d8;
--ink-soft: #a49d8b;
--line: #332f25;
--red: #e0694e;
--gold: #d0b25f;
--doc: #6fbf8f;
--arc: #d9b25f;
color-scheme: dark;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: "Newsreader", Georgia, serif;
font-size: 16px;
line-height: 1.5;
}
.wrap { max-width: 1160px; margin: 0 auto; padding-inline: 20px; }
/* masthead */
header.masthead {
border-bottom: 3px solid var(--ink);
padding-block: 26px 18px;
text-align: center;
}
.kicker {
font-family: "Oswald", system-ui, sans-serif;
font-weight: 500;
letter-spacing: .28em;
text-transform: uppercase;
font-size: 11px;
color: var(--red);
margin-bottom: 6px;
}
header.masthead h1 {
font-family: "Oswald", system-ui, sans-serif;
font-weight: 700;
letter-spacing: .01em;
font-size: clamp(30px, 6vw, 56px);
line-height: .98;
margin: 0;
text-transform: uppercase;
text-wrap: balance;
}
header.masthead .life { font-weight: 300; color: var(--ink-soft); }
header.masthead .dek {
max-width: 60ch;
margin: 12px auto 0;
color: var(--ink-soft);
font-size: 15px;
}
.prizes {
display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;
margin-top: 14px;
}
.prize {
font-family: "Oswald", system-ui, sans-serif;
font-size: 12px; letter-spacing: .08em; text-transform: uppercase;
border: 1px solid var(--gold); color: var(--gold);
padding: 4px 10px; border-radius: 2px;
}
/* controls */
.toolbar {
position: sticky; top: env(safe-area-inset-top, 0px); z-index: 5;
background: var(--paper);
border-bottom: 1px solid var(--line);
display: flex; flex-wrap: wrap; align-items: center; gap: 14px 18px;
padding-block: 12px;
}
.toolbar label {
font-family: "Oswald", system-ui, sans-serif;
font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
color: var(--ink-soft); margin-right: 6px;
}
select, input[type="search"] {
font-family: "Newsreader", Georgia, serif; font-size: 15px;
background: var(--card); color: var(--ink);
border: 1px solid var(--line); border-radius: 2px; padding: 6px 9px;
}
input[type="search"] { min-width: 190px; flex: 1 1 190px; }
input[type="range"] { accent-color: var(--red); width: 120px; vertical-align: middle; }
.count { margin-left: auto; font-family: "Oswald", system-ui, sans-serif; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-soft); }
/* facet chips */
.facets { display: flex; flex-wrap: wrap; gap: 8px; padding-block: 14px 4px; }
.chip {
font-family: "Oswald", system-ui, sans-serif;
font-size: 12px; letter-spacing: .05em;
background: none; color: var(--ink-soft);
border: 1px solid var(--line); border-radius: 999px;
padding: 4px 12px; cursor: pointer;
}
.chip[aria-pressed="true"] { color: var(--paper); background: var(--ink); border-color: var(--ink); }
.chip.clear { border-style: dashed; }
/* grid */
main { padding-block: 8px 60px; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--col, 260px), 1fr));
gap: 16px;
margin-top: 12px;
}
.card {
background: var(--card);
border: 1px solid var(--line);
border-left: 3px solid var(--rail, var(--line));
border-radius: 3px;
padding: 14px 16px;
cursor: pointer;
display: flex; flex-direction: column; gap: 8px;
text-align: left;
color: inherit; font: inherit;
}
.card:hover { border-color: var(--ink-soft); }
.card:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
.card[data-rt="documented"] { --rail: var(--doc); }
.card[data-rt="collection-summary"] { --rail: var(--arc); }
.card[data-award="1"] { --rail: var(--red); }
.card .date {
font-family: "Oswald", system-ui, sans-serif;
font-size: 12px; letter-spacing: .06em; color: var(--ink-soft);
font-variant-numeric: tabular-nums;
}
.card .title { font-size: 18px; font-weight: 600; line-height: 1.25; }
.card .subj { font-size: 14px; color: var(--ink-soft); flex: 1; }
.card .tags { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 2px; }
.tag {
font-family: "Oswald", system-ui, sans-serif;
font-size: 10px; letter-spacing: .08em; text-transform: uppercase;
padding: 2px 7px; border-radius: 2px; color: var(--paper);
}
.tag.documented { background: var(--doc); }
.tag.collection-summary { background: var(--arc); }
.tag.award { background: var(--red); }
.tag.pub { background: none; color: var(--ink-soft); border: 1px solid var(--line); }
.empty { padding: 50px 0; text-align: center; color: var(--ink-soft); font-style: italic; }
/* modal */
.backdrop {
position: fixed; inset: 0; background: rgba(0,0,0,.55);
display: none; align-items: center; justify-content: center; padding: 20px; z-index: 40;
}
.backdrop.open { display: flex; }
.modal {
background: var(--card); color: var(--ink);
border: 1px solid var(--line); border-top: 4px solid var(--red);
border-radius: 4px; max-width: 640px; width: 100%;
max-height: 86vh; overflow-y: auto; padding: 24px 26px; position: relative;
}
.modal .x {
position: absolute; top: 12px; right: 14px; background: none; border: none;
font-size: 24px; line-height: 1; cursor: pointer; color: var(--ink-soft);
}
.modal .m-kicker { font-family: "Oswald", system-ui, sans-serif; font-size: 12px; letter-spacing: .1em; text-transform: uppercase; color: var(--red); }
.modal h2 { font-family: "Oswald", system-ui, sans-serif; font-weight: 700; font-size: 26px; margin: 4px 0 2px; text-wrap: balance; }
.modal .m-date { color: var(--ink-soft); font-size: 14px; margin-bottom: 16px; font-variant-numeric: tabular-nums; }
.modal dl { display: grid; grid-template-columns: 120px 1fr; gap: 8px 16px; margin: 0; font-size: 15px; }
.modal dt { font-family: "Oswald", system-ui, sans-serif; font-size: 12px; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-soft); padding-top: 2px; }
.modal dd { margin: 0; }
.modal a { color: var(--red); }
.modal .rights { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--line); font-size: 13px; font-style: italic; color: var(--ink-soft); }
footer { border-top: 1px solid var(--line); padding-block: 20px 30px; color: var(--ink-soft); font-size: 13px; }
footer a { color: var(--red); }
@media (max-width: 520px) {
.modal dl { grid-template-columns: 1fr; gap: 2px; }
.modal dt { margin-top: 8px; }
}
</style>
<header class="masthead">
<div class="wrap">
<div class="kicker">Catalogue of an Editorial Cartoonist</div>
<h1>Paul Conrad <span class="life">1924–2010</span></h1>
<p class="dek" id="dek"></p>
<div class="prizes" id="prizes"></div>
</div>
</header>
<div class="wrap">
<div class="toolbar">
<span><label for="q">Search</label><input type="search" id="q" placeholder="title, subject, year…"></span>
<span><label for="sort">Sort</label>
<select id="sort">
<option value="year-asc">Year ↑</option>
<option value="year-desc">Year ↓</option>
<option value="title">Title A–Z</option>
<option value="award">Prizes first</option>
</select>
</span>
<span><label for="density">Density</label><input type="range" id="density" min="200" max="380" step="20" value="260"></span>
<span class="count" id="count"></span>
</div>
<div class="facets" id="facets"></div>
</div>
<main class="wrap">
<div class="grid" id="grid"></div>
<div class="empty" id="empty" hidden>No entries match these filters.</div>
</main>
<footer class="wrap">
<p><strong>On the images.</strong> No public-domain or open-license Paul Conrad cartoon exists — every work is copyright the Los Angeles Times or the Conrad estate. This index therefore hosts no cartoon images; each entry links to the holding archive or retrospective where the work can be viewed.</p>
<p id="sources"></p>
</footer>
<div class="backdrop" id="backdrop"><div class="modal" id="modal" role="dialog" aria-modal="true"></div></div>
<script>
const DATA = /*__DATA__*/{};
const BIO = DATA.bio || {};
const RECORDS = (DATA.records || []).map((r, i) => ({
id: r.id || ('r'+i),
title: r.title || 'Untitled',
date: r.date || r.date_range || '',
year: (String(r.date || r.date_range || '').match(/\d{4}/) || ['—'])[0],
publication: r.publication || '',
subject: r.subject || '',
award: r.award || '',
record_type: r.record_type || 'documented',
extent: r.extent || '',
rights_note: r.rights_note || '',
citation_url: r.citation_url || ''
}));
const esc = s => String(s==null?'':s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const el = id => document.getElementById(id);
let filters = { year:null, publication:null, record_type:null, awardOnly:false };
// masthead
el('dek').textContent = BIO.career
? (BIO.career + '. ' + (BIO.honors ? BIO.honors.split(';')[0] + '.' : ''))
: 'Three-time Pulitzer Prize–winning editorial cartoonist.';
el('prizes').innerHTML = ['1964','1971','1984'].map(y => '<span class="prize">Pulitzer '+y+'</span>').join('');
// facets
function facetValues(field) {
const m = new Map();
for (const r of RECORDS) { const v = r[field]; if (v) m.set(v, (m.get(v)||0)+1); }
return [...m.entries()].sort((a,b)=> b[1]-a[1] || String(a[0]).localeCompare(String(b[0])));
}
function renderFacets() {
const parts = [];
parts.push('<button class="chip" data-f="awardOnly" aria-pressed="'+(filters.awardOnly?'true':'false')+'">Pulitzer years</button>');
for (const rt of ['documented','collection-summary']) {
const n = RECORDS.filter(r=>r.record_type===rt).length; if (!n) continue;
const label = rt==='documented' ? 'Documented' : 'Archive holdings';
parts.push('<button class="chip" data-f="record_type" data-v="'+rt+'" aria-pressed="'+(filters.record_type===rt?'true':'false')+'">'+label+' · '+n+'</button>');
}
for (const [v,n] of facetValues('publication')) {
parts.push('<button class="chip" data-f="publication" data-v="'+esc(v)+'" aria-pressed="'+(filters.publication===v?'true':'false')+'">'+esc(v)+' · '+n+'</button>');
}
if (filters.year || filters.publication || filters.record_type || filters.awardOnly)
parts.push('<button class="chip clear" data-f="clear">Clear</button>');
const f = el('facets'); f.innerHTML = parts.join('');
f.querySelectorAll('.chip').forEach(c => c.addEventListener('click', () => {
const k = c.dataset.f;
if (k === 'clear') filters = { year:null, publication:null, record_type:null, awardOnly:false };
else if (k === 'awardOnly') filters.awardOnly = !filters.awardOnly;
else filters[k] = filters[k] === c.dataset.v ? null : c.dataset.v;
renderFacets(); render();
}));
}
function applied() {
const q = el('q').value.trim().toLowerCase();
return RECORDS.filter(r => {
if (filters.publication && r.publication !== filters.publication) return false;
if (filters.record_type && r.record_type !== filters.record_type) return false;
if (filters.awardOnly && !r.award) return false;
if (q) {
const hay = [r.title, r.subject, r.publication, r.award, r.date].join(' ').toLowerCase();
if (!hay.includes(q)) return false;
}
return true;
});
}
function sorted(list) {
const a = [...list], m = el('sort').value;
if (m==='year-asc') a.sort((x,y)=> x.year.localeCompare(y.year) || x.title.localeCompare(y.title));
else if (m==='year-desc') a.sort((x,y)=> y.year.localeCompare(x.year) || x.title.localeCompare(y.title));
else if (m==='title') a.sort((x,y)=> x.title.localeCompare(y.title));
else if (m==='award') a.sort((x,y)=> (y.award?1:0)-(x.award?1:0) || x.year.localeCompare(y.year));
return a;
}
function render() {
const list = sorted(applied());
el('count').textContent = list.length + ' of ' + RECORDS.length;
const g = el('grid');
el('empty').hidden = list.length !== 0;
g.innerHTML = list.map(r => {
const tags = [
'<span class="tag '+r.record_type+'">'+(r.record_type==='documented'?'Documented':'Archive')+'</span>',
r.award ? '<span class="tag award">Pulitzer</span>' : '',
r.publication ? '<span class="tag pub">'+esc(r.publication)+'</span>' : ''
].join('');
return '<button class="card" data-id="'+r.id+'" data-rt="'+r.record_type+'" data-award="'+(r.award?1:0)+'">'
+ '<span class="date">'+esc(r.date || r.year)+'</span>'
+ '<span class="title">'+esc(r.title)+'</span>'
+ '<span class="subj">'+esc(r.subject)+'</span>'
+ '<span class="tags">'+tags+'</span></button>';
}).join('');
g.querySelectorAll('.card').forEach(c => c.addEventListener('click', () => openModal(c.dataset.id)));
}
function openModal(id) {
const r = RECORDS.find(x => x.id === id); if (!r) return;
el('modal').innerHTML =
'<button class="x" id="mx" aria-label="Close">×</button>'
+ '<div class="m-kicker">'+(r.record_type==='documented'?'Cartoon':'Archive holding')+(r.award?' · Pulitzer':'')+'</div>'
+ '<h2>'+esc(r.title)+'</h2>'
+ '<div class="m-date">'+esc(r.date || r.year)+(r.publication?' · '+esc(r.publication):'')+'</div>'
+ '<dl>'
+ '<dt>Subject</dt><dd>'+esc(r.subject||'—')+'</dd>'
+ (r.award?'<dt>Award</dt><dd>'+esc(r.award)+'</dd>':'')
+ (r.extent?'<dt>Extent</dt><dd>'+esc(r.extent)+'</dd>':'')
+ '<dt>Source</dt><dd>'+(r.citation_url?'<a href="'+esc(r.citation_url)+'" target="_blank" rel="noopener noreferrer">'+esc(r.citation_url.replace(/^https?:\/\//,'').split('/')[0])+' ↗</a>':'—')+'</dd>'
+ '</dl>'
+ (r.rights_note?'<div class="rights">'+esc(r.rights_note)+'</div>':'');
el('mx').addEventListener('click', closeModal);
el('backdrop').classList.add('open');
}
function closeModal(){ el('backdrop').classList.remove('open'); }
el('backdrop').addEventListener('click', e => { if (e.target.id==='backdrop') closeModal(); });
document.addEventListener('keydown', e => { if (e.key==='Escape') closeModal(); });
el('q').addEventListener('input', render);
el('sort').addEventListener('change', render);
el('density').addEventListener('input', () => document.documentElement.style.setProperty('--col', el('density').value + 'px'));
document.documentElement.style.setProperty('--col', el('density').value + 'px');
// sources line
const srcHosts = [...new Set(RECORDS.map(r=>r.citation_url).filter(Boolean).map(u=>u.replace(/^https?:\/\//,'').split('/')[0]))];
el('sources').innerHTML = 'Compiled from ' + srcHosts.map(h=>esc(h)).join(' · ') + '.';
renderFacets();
render();
</script>