PSA-slab theme: render the verdict as a slab label, matching grade chips in history

This commit is contained in:
Barely Removable 2026-08-22 12:31:15 -07:00
parent 3c98373ad6
commit f910a603c3
2 changed files with 96 additions and 22 deletions

View file

@ -206,10 +206,16 @@ function renderSlab(g, opts = {}) {
? `realistically ${g.grade_low}–${g.grade_high}` : '';
const title = opts.title || g.card_note || 'Card';
const bits = [range, `${g.confidence || ''} confidence`].filter(Boolean).join(' · ');
// The kicker sits where a real slab label carries its grader's logo. It
// says ESTIMATE first and always — the label is styled to look like the
// real thing, so it has to say plainly that it isn't one.
const kicker = ['Grade estimate', g.card_type ? (TYPE_LABEL[g.card_type] || 'Card') : '']
.filter(Boolean).join(' · ');
return `
<div class="slab ${slabClass(grade)}">
<div class="slab-main">
${g.card_type ? `<div><span class="card-type-tag">${esc(TYPE_LABEL[g.card_type] || 'Card')}</span></div>` : ''}
<div class="slab-kicker">${esc(kicker)}</div>
${opts.hideTitle ? '' : `<div class="slab-title">${esc(title)}</div>`}
<div class="slab-sub">${esc(bits)}</div>
</div>
@ -335,6 +341,17 @@ function gradePillClass(grade) {
return 'pill-critical';
}
// Mirrors slabClass so a history chip and that card's slab always land on
// the same colour band — they're the same verdict shown at two sizes.
function gradeChipClass(grade) {
if (grade === null || grade === undefined) return 'grade-chip-na';
if (grade >= 10) return 'grade-chip-10';
if (grade === 9) return 'grade-chip-9';
if (grade >= 7) return 'grade-chip-8';
if (grade >= 5) return 'grade-chip-6';
return 'grade-chip-low';
}
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
@ -370,7 +387,7 @@ function renderHistory() {
</div>
</div>
</td>
<td><span class="pill ${gradePillClass(g.estimated_grade)}">${
<td><span class="grade-chip ${gradeChipClass(g.estimated_grade)}">${
g.estimated_grade === null ? 'n/a' : `PSA ${g.estimated_grade}`}</span></td>
<td><span class="cell-conf conf-${esc(g.confidence || 'low')}">${esc(g.confidence || '')}</span></td>
<td class="cardcell-meta cell-when">${esc(fmtWhen(g.created_at))}</td>

View file

@ -32,6 +32,16 @@
--accent-2: #3ddcc0;
--accent-soft: rgba(77, 155, 255, .14);
/* Label chrome only — the red band on a slab label, never a severity.
Deliberately deeper than --critical (#f2555a) so the two don't read as
the same signal: this one is decoration on a physical-looking object,
that one means "distrust this number". */
--slab-red: #c8102e;
--slab-face: #eceef2;
--slab-ink: #0c111a;
--slab-ink-2: #5a6474;
--slab-rule: #b9c0cc;
--good: #23c55e;
--good-text: #4ade80;
--warning: #f5a33c;
@ -419,25 +429,53 @@ textarea { resize: vertical; min-height: 64px; }
than as one more statistic on the page. Promoted to a hero element here:
full width, bigger numerals, real elevation. */
/* Rendered as a graded slab's LABEL: light face, dark text, a red band
along the top where a real label carries its logo, and the grade boxed
off on the right. Against this dark UI a light label reads as a physical
object sitting on the page, which is the point — the verdict should look
like the thing it's estimating, not like another panel.
The band deliberately carries "GRADE ESTIMATE" rather than any grader's
mark: it keeps the label honest about what it is at a glance, which
matters more here than the resemblance does. */
.slab {
position: relative;
display: flex; align-items: stretch; gap: 0;
border-radius: var(--radius); overflow: hidden;
border: 1px solid var(--rule);
background: var(--surface-2);
border-radius: 7px; overflow: hidden;
border: 1px solid var(--slab-rule);
background: linear-gradient(178deg, #fbfbfc 0%, var(--slab-face) 58%, #dde1e9 100%);
color: var(--slab-ink);
box-shadow: var(--shadow-lg);
width: 100%;
}
.slab-main {
flex: 1; min-width: 0; padding: 20px 22px;
display: flex; flex-direction: column; justify-content: center; gap: 6px;
.slab::before {
content: ''; position: absolute; left: 0; right: 0; top: 0; height: 3px;
background: var(--slab-red);
}
.slab-title { font-size: 17px; font-weight: 700; line-height: 1.3; letter-spacing: -.01em; }
.slab-sub { font-size: 12.5px; color: var(--muted); }
.slab-main {
flex: 1; min-width: 0; padding: 17px 20px 15px;
display: flex; flex-direction: column; justify-content: center; gap: 5px;
}
.slab-kicker {
display: flex; align-items: center; gap: 7px;
font-size: 9.5px; font-weight: 800; letter-spacing: .15em;
text-transform: uppercase; color: var(--slab-red);
}
.slab-kicker::before {
content: ''; width: 12px; height: 12px; border-radius: 2.5px; flex: none;
background: var(--slab-red);
}
.slab-title {
font-size: 16px; font-weight: 700; line-height: 1.3; letter-spacing: -.01em;
color: var(--slab-ink);
}
.slab-sub { font-size: 12px; color: var(--slab-ink-2); }
.slab-grade {
position: relative; overflow: hidden;
flex: none; width: 148px;
flex: none; width: 136px;
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 20px 10px; color: #fff; text-align: center;
padding: 18px 10px; color: #fff; text-align: center;
border-left: 1px solid var(--slab-rule);
background: linear-gradient(150deg, #1b4f8f, #2f6fd0);
}
.slab-grade .n {
@ -478,12 +516,31 @@ textarea { resize: vertical; min-height: 64px; }
.slab-10 .slab-grade::after { animation: none; opacity: .25; }
}
.card-type-tag {
display: inline-block; font-size: 10px; font-weight: 800;
letter-spacing: .1em; text-transform: uppercase;
padding: 3px 9px; border-radius: 5px;
background: var(--accent-soft); color: #8fc0ff;
border: 1px solid color-mix(in srgb, var(--accent) 38%, transparent);
/* Card type now rides in the label's kicker line rather than as a separate
chip — one fewer element competing inside a label that should read as a
single printed artifact. */
/* History grade chips: the slab's grade block in miniature, on the same
colour bands, so a scan down the list reads as a row of slabs. Kept
separate from .pill because those carry SEVERITY in the category table —
same shape, unrelated meaning, so they must not share styling. */
.grade-chip {
display: inline-flex; align-items: center; justify-content: center;
min-width: 54px; padding: 5px 9px; border-radius: 5px;
font-size: 12px; font-weight: 800; letter-spacing: .03em;
color: #fff; white-space: nowrap;
background: linear-gradient(150deg, #1b4f8f, #2f6fd0);
}
.grade-chip-9 { background: linear-gradient(150deg, #0b6d33, #16a34a); }
.grade-chip-8 { background: linear-gradient(150deg, #1b4f8f, #2f6fd0); }
.grade-chip-6 { background: linear-gradient(150deg, #9a5c0b, #e0891f); }
.grade-chip-low { background: linear-gradient(150deg, #8f1f24, #d13a40); }
.grade-chip-10 {
background: linear-gradient(140deg, #0b6d33, #3ddcc0 38%, #4d9bff 66%, #a97bff);
}
.grade-chip-na {
background: var(--surface-2); color: var(--muted);
border: 1px solid var(--rule); font-weight: 700;
}
/* --------------------------------------------------------------- phone */
@ -503,10 +560,10 @@ textarea { resize: vertical; min-height: 64px; }
/* The slab stays horizontal — stacking it would bury the number, which is
the one thing someone opens this app to see. Just tighter. */
.slab-main { padding: 15px 16px; }
.slab-title { font-size: 15.5px; }
.slab-grade { width: 108px; padding: 16px 8px; }
.slab-grade .n { font-size: 44px; }
.slab-main { padding: 14px 15px 13px; }
.slab-title { font-size: 15px; }
.slab-grade { width: 98px; padding: 15px 8px; }
.slab-grade .n { font-size: 42px; }
}
/* On a narrow phone the five-column history table forces sideways