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}` : ''; ? `realistically ${g.grade_low}${g.grade_high}` : '';
const title = opts.title || g.card_note || 'Card'; const title = opts.title || g.card_note || 'Card';
const bits = [range, `${g.confidence || ''} confidence`].filter(Boolean).join(' · '); 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 ` return `
<div class="slab ${slabClass(grade)}"> <div class="slab ${slabClass(grade)}">
<div class="slab-main"> <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>`} ${opts.hideTitle ? '' : `<div class="slab-title">${esc(title)}</div>`}
<div class="slab-sub">${esc(bits)}</div> <div class="slab-sub">${esc(bits)}</div>
</div> </div>
@ -335,6 +341,17 @@ function gradePillClass(grade) {
return 'pill-critical'; 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', const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
@ -370,7 +387,7 @@ function renderHistory() {
</div> </div>
</div> </div>
</td> </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> 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><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> <td class="cardcell-meta cell-when">${esc(fmtWhen(g.created_at))}</td>

View file

@ -32,6 +32,16 @@
--accent-2: #3ddcc0; --accent-2: #3ddcc0;
--accent-soft: rgba(77, 155, 255, .14); --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: #23c55e;
--good-text: #4ade80; --good-text: #4ade80;
--warning: #f5a33c; --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: than as one more statistic on the page. Promoted to a hero element here:
full width, bigger numerals, real elevation. */ 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 { .slab {
position: relative;
display: flex; align-items: stretch; gap: 0; display: flex; align-items: stretch; gap: 0;
border-radius: var(--radius); overflow: hidden; border-radius: 7px; overflow: hidden;
border: 1px solid var(--rule); border: 1px solid var(--slab-rule);
background: var(--surface-2); background: linear-gradient(178deg, #fbfbfc 0%, var(--slab-face) 58%, #dde1e9 100%);
color: var(--slab-ink);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
width: 100%; width: 100%;
} }
.slab-main { .slab::before {
flex: 1; min-width: 0; padding: 20px 22px; content: ''; position: absolute; left: 0; right: 0; top: 0; height: 3px;
display: flex; flex-direction: column; justify-content: center; gap: 6px; background: var(--slab-red);
} }
.slab-title { font-size: 17px; font-weight: 700; line-height: 1.3; letter-spacing: -.01em; } .slab-main {
.slab-sub { font-size: 12.5px; color: var(--muted); } 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 { .slab-grade {
position: relative; overflow: hidden; position: relative; overflow: hidden;
flex: none; width: 148px; flex: none; width: 136px;
display: flex; flex-direction: column; align-items: center; justify-content: center; 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); background: linear-gradient(150deg, #1b4f8f, #2f6fd0);
} }
.slab-grade .n { .slab-grade .n {
@ -478,12 +516,31 @@ textarea { resize: vertical; min-height: 64px; }
.slab-10 .slab-grade::after { animation: none; opacity: .25; } .slab-10 .slab-grade::after { animation: none; opacity: .25; }
} }
.card-type-tag { /* Card type now rides in the label's kicker line rather than as a separate
display: inline-block; font-size: 10px; font-weight: 800; chip one fewer element competing inside a label that should read as a
letter-spacing: .1em; text-transform: uppercase; single printed artifact. */
padding: 3px 9px; border-radius: 5px;
background: var(--accent-soft); color: #8fc0ff; /* History grade chips: the slab's grade block in miniature, on the same
border: 1px solid color-mix(in srgb, var(--accent) 38%, transparent); 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 */ /* --------------------------------------------------------------- phone */
@ -503,10 +560,10 @@ textarea { resize: vertical; min-height: 64px; }
/* The slab stays horizontal stacking it would bury the number, which is /* The slab stays horizontal stacking it would bury the number, which is
the one thing someone opens this app to see. Just tighter. */ the one thing someone opens this app to see. Just tighter. */
.slab-main { padding: 15px 16px; } .slab-main { padding: 14px 15px 13px; }
.slab-title { font-size: 15.5px; } .slab-title { font-size: 15px; }
.slab-grade { width: 108px; padding: 16px 8px; } .slab-grade { width: 98px; padding: 15px 8px; }
.slab-grade .n { font-size: 44px; } .slab-grade .n { font-size: 42px; }
} }
/* On a narrow phone the five-column history table forces sideways /* On a narrow phone the five-column history table forces sideways