The Charizard grade that prompted this had the model writing 'possible non-standard/proxy print' directly into card_note -- the only free-text field available -- since there was nowhere else for that observation to go. That's why it got no visual treatment: the UI renders card_note as a plain grey subtitle, identical to any ordinary card name. Added authenticity as its own top-level field (flag: none/worth_checking/ likely_not_genuine, plus an observation) alongside the four grading categories but explicitly separate from them -- this is about whether the card is a genuine product at all, not its condition, and PSA authenticates before it grades, so a flagged card's estimated_grade goes to null rather than a number. Prompted with concrete tells for both classes of concern: fan-made/proxy prints (fictional sets, non-existent number combos, home-printer texture, 'proxy' watermarks) and counterfeits of real cards (colour/font/holo mismatches against genuine copies). Trimming (already covered under CORNERS from an earlier fix) now also sets this field when flagged, so the UI has one place to check regardless of which specific issue triggered it. UI: a full banner (not a hint line) above the slab -- critical red for likely_not_genuine, warning amber for worth_checking -- plus a matching dot next to the card name in History so it's visible without opening the card. Both colours reuse --critical/--warning rather than the brand red, since this is exactly the 'distrust this' signal those are already reserved for. Migration tested against the live schema shape; existing rows get authenticity=None and render with no banner, as before.
628 lines
25 KiB
CSS
628 lines
25 KiB
CSS
/* Card Grader — slab-house theme: silver ground, red accent, black text.
|
||
Card-type agnostic (Pokemon, sports, TCG).
|
||
|
||
Committed light, not a light *mode*. A single deliberate look; a
|
||
half-tended second palette is how a design drifts out of sync with
|
||
itself. Phones in dark mode get this same design, which is the intent.
|
||
|
||
Colour discipline:
|
||
- Grade severities ride a lightness ladder (none -> minor -> moderate ->
|
||
major), so the table still reads as escalating severity in greyscale.
|
||
- Red is the BRAND here, so it can no longer also mean "distrust this
|
||
number". Severity red is therefore a distinctly darker, browner red
|
||
(--critical) than the brand's (--brand), and severity is never carried
|
||
by colour alone — the pills and chips carry a word too.
|
||
- Foil/holo treatment is reserved for a 10. Every card shimmering would
|
||
say nothing. */
|
||
|
||
:root {
|
||
color-scheme: light;
|
||
|
||
/* Ground is a faint cool grey rather than pure white — slab labels, card
|
||
thumbnails and the panels all sit ON something, and pure white leaves
|
||
nothing for a white label to separate from. */
|
||
--ground: #eef0f4;
|
||
--surface-1: #ffffff;
|
||
--surface-2: #f4f6f9; /* raised: modal insets, slab body, hover */
|
||
--ink: #10141c;
|
||
--ink-2: #47505f;
|
||
--muted: #6b7484;
|
||
--hairline: #e2e6ec;
|
||
--rule: #ccd2dc;
|
||
|
||
/* PSA-style red. Used for brand chrome and primary actions. */
|
||
--brand: #c8102e;
|
||
--brand-dark: #a20d25;
|
||
--brand-soft: rgba(200, 16, 46, .09);
|
||
|
||
--accent: var(--brand);
|
||
--accent-2: #1c2b45; /* deep navy — the label's text/secondary tone */
|
||
--accent-soft: var(--brand-soft);
|
||
|
||
/* Slab label tokens. On a light ground the label is near-white and
|
||
separates by its border and shadow rather than by being lighter than
|
||
everything else, which is how a real label sits on a page. */
|
||
--slab-red: var(--brand);
|
||
--slab-face: #f7f8fa;
|
||
--slab-ink: #0c111a;
|
||
--slab-ink-2: #5a6474;
|
||
--slab-rule: #c2c8d2;
|
||
|
||
--good: #16853f;
|
||
/* These two are the TEXT colour inside severity pills, which sit on a 15%
|
||
tint of themselves — a much lighter ground than the white they were
|
||
first picked against. Measured in place, the original pair came in at
|
||
4.38:1 and 3.37:1, both under the 4.5:1 AA floor for 12px text, on the
|
||
labels that carry this app's actual verdict. Darkened until they pass
|
||
with margin. */
|
||
--good-text: #0d5c2a;
|
||
--warning: #8a5000;
|
||
/* Deliberately darker and browner than --brand so a severity red never
|
||
reads as the brand red sitting next to it. */
|
||
--critical: #8f1d1d;
|
||
|
||
--ring: rgba(16, 20, 28, .12);
|
||
--shadow-sm: 0 1px 2px rgba(16, 20, 28, .08);
|
||
--shadow: 0 1px 2px rgba(16, 20, 28, .06), 0 6px 18px rgba(16, 20, 28, .08);
|
||
--shadow-lg: 0 2px 6px rgba(16, 20, 28, .08), 0 18px 44px rgba(16, 20, 28, .14);
|
||
|
||
--radius: 14px;
|
||
--radius-lg: 18px;
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
|
||
body {
|
||
margin: 0;
|
||
color: var(--ink);
|
||
font: 16px/1.55 "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||
-webkit-font-smoothing: antialiased;
|
||
text-rendering: optimizeLegibility;
|
||
|
||
/* A single very faint warm-red wash at the top, so the ground isn't a
|
||
flat slab of grey but doesn't compete with the content either. The
|
||
dark theme's two coloured glows were far too strong to survive on a
|
||
light ground — they read as stains rather than depth. */
|
||
background-color: var(--ground);
|
||
background-image:
|
||
radial-gradient(1100px 520px at 50% -12%, rgba(200, 16, 46, .06), transparent 70%);
|
||
background-attachment: fixed;
|
||
background-repeat: no-repeat;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- type */
|
||
|
||
h1, h2, h3 { margin: 0; font-weight: 650; }
|
||
h1 { font-size: 17px; letter-spacing: -.01em; }
|
||
h2 { font-size: 21px; letter-spacing: -.02em; }
|
||
h3 {
|
||
font-size: 12px; text-transform: uppercase; letter-spacing: .08em;
|
||
color: var(--muted); font-weight: 700;
|
||
}
|
||
|
||
main {
|
||
max-width: 940px;
|
||
margin: 0 auto;
|
||
padding: 30px 24px 96px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 22px;
|
||
}
|
||
|
||
/* ------------------------------------------------------------- topbar */
|
||
|
||
/* The sticky unit is the header + banner together (see index.html) — the
|
||
banner used to sticky itself to a hardcoded top offset matching the
|
||
header's height, which broke the moment the header grew. */
|
||
.chrome { position: sticky; top: 0; z-index: 20; }
|
||
|
||
.topbar {
|
||
position: relative; /* containing block for the holo edge below */
|
||
background: rgba(255, 255, 255, .88);
|
||
-webkit-backdrop-filter: blur(14px) saturate(1.3);
|
||
backdrop-filter: blur(14px) saturate(1.3);
|
||
border-bottom: 1px solid var(--rule);
|
||
}
|
||
/* The bar spans the viewport, but its CONTENTS line up with the page
|
||
column — the logo sitting hard against a 3440px screen edge while the
|
||
panels floated in a narrow centre column was the single thing making
|
||
this read as unfinished rather than designed. */
|
||
.topbar-inner {
|
||
max-width: 940px; margin: 0 auto;
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
gap: 16px; flex-wrap: wrap;
|
||
padding: 13px 24px;
|
||
}
|
||
/* Holo-foil edge: the rainbow sweep you get tilting a refractor. Purely
|
||
decorative and it encodes nothing, so it's free to be vivid here where no
|
||
number lives — but dialled back from the original full-strength stripe,
|
||
which was the loudest thing on an otherwise restrained page. */
|
||
.topbar::after {
|
||
content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px;
|
||
background: linear-gradient(90deg,
|
||
#ff5f8f, #ffb347, #ffe66d, #3ddcc0, #4d9bff, #a97bff, #ff5f8f);
|
||
opacity: .8; /* light ground needs more of it than dark did to register */
|
||
}
|
||
.topbar h1 {
|
||
color: var(--ink); font-size: 16px; font-weight: 700; letter-spacing: -.005em;
|
||
}
|
||
|
||
/* The header's buttons inherit the page's button styling now. On the dark
|
||
theme they needed their own palette to stay legible against the bar;
|
||
against a white bar they don't, and overriding them only made the header
|
||
inconsistent with every other control. */
|
||
.topbar .btn-quiet:hover { background: var(--surface-2); filter: none; }
|
||
|
||
.brand { display: flex; align-items: center; gap: 11px; }
|
||
|
||
/* Brand mark: a graded slab — case, label, and the card sealed inside.
|
||
Drawn as flat two-tone geometry rather than the gradient-blob-with-a-
|
||
checkmark it replaced: that read as a generic app badge and said nothing
|
||
about grading. Thin strokes and no glow keep it legible at 22px. */
|
||
.brand-mark { flex: none; width: 18px; height: 27px; display: block; }
|
||
/* Case in the label's navy, label band in brand red — the same two-tone
|
||
relationship a real slab label has, and it keeps the mark legible
|
||
against a white bar where an all-red mark would shout. */
|
||
.brand-mark .mark-case { stroke: var(--accent-2); }
|
||
.brand-mark .mark-label { fill: var(--brand); }
|
||
/* .48, not the .34 this started at — below roughly .45 the card window
|
||
stops separating from the case interior at topbar size and the mark
|
||
collapses into an empty outline. Checked by rendering it at 42px. */
|
||
.brand-mark .mark-card { fill: var(--accent-2); opacity: .48; }
|
||
|
||
.topbar-actions { display: flex; gap: 9px; flex-wrap: wrap; }
|
||
|
||
/* ------------------------------------------------------------ controls */
|
||
|
||
/* Flat accent fill with a plain shadow. The previous gradient-plus-coloured-
|
||
glow made every button feel like the page's main event, which flattened
|
||
the hierarchy — the grade result should be the loudest thing here, not
|
||
the controls. */
|
||
.btn {
|
||
font: inherit; font-weight: 600; font-size: 14.5px;
|
||
padding: 9px 17px; border-radius: 9px; cursor: pointer;
|
||
background: var(--brand);
|
||
color: #fff;
|
||
border: 1px solid transparent;
|
||
text-decoration: none; display: inline-block; line-height: 1.4;
|
||
white-space: nowrap;
|
||
box-shadow: var(--shadow-sm);
|
||
transition: filter .13s ease, transform .13s ease, background .13s ease;
|
||
}
|
||
.btn:hover { background: var(--brand-dark); filter: none; }
|
||
.btn:active { transform: translateY(1px); }
|
||
.btn[disabled] { opacity: .45; cursor: default; filter: none; box-shadow: none; }
|
||
.btn:focus-visible, .close:focus-visible {
|
||
outline: 2px solid var(--accent-2); outline-offset: 2px;
|
||
}
|
||
|
||
.btn-quiet {
|
||
background: var(--surface-1); color: var(--ink);
|
||
border-color: var(--rule); box-shadow: var(--shadow-sm);
|
||
}
|
||
.btn-quiet:hover { background: var(--surface-2); filter: none; }
|
||
|
||
.btn-danger {
|
||
background: transparent; color: var(--critical);
|
||
border-color: color-mix(in srgb, var(--critical) 55%, transparent);
|
||
box-shadow: none;
|
||
}
|
||
.btn-danger:hover { background: var(--critical); color: #fff; filter: none; }
|
||
|
||
.btn-sm { padding: 5px 12px; font-size: 13px; }
|
||
|
||
.input, select, textarea {
|
||
font: inherit; font-size: 14.5px;
|
||
padding: 9px 12px;
|
||
border: 1px solid var(--rule);
|
||
border-radius: 10px;
|
||
background: var(--surface-1);
|
||
color: var(--ink);
|
||
width: 100%;
|
||
transition: border-color .13s ease, box-shadow .13s ease;
|
||
}
|
||
.input::placeholder, textarea::placeholder { color: var(--muted); }
|
||
.input:focus, select:focus, textarea:focus {
|
||
outline: none;
|
||
border-color: var(--brand);
|
||
box-shadow: 0 0 0 3px var(--brand-soft);
|
||
}
|
||
textarea { resize: vertical; min-height: 64px; }
|
||
|
||
/* --------------------------------------------------------------- tiles */
|
||
|
||
.tile-value { font-size: 27px; font-weight: 750; letter-spacing: -.02em; }
|
||
.tile-sub { font-size: 12.5px; color: var(--ink-2); margin-top: 4px; }
|
||
.pos { color: var(--good-text); }
|
||
.neg { color: var(--critical); }
|
||
|
||
/* -------------------------------------------------------------- panels */
|
||
|
||
/* The dark theme lit each panel with a top white-gradient highlight; on a
|
||
white surface that's invisible, so panels separate from the ground by
|
||
border and shadow instead. */
|
||
.panel {
|
||
position: relative;
|
||
background: var(--surface-1);
|
||
border: 1px solid var(--hairline);
|
||
border-radius: var(--radius-lg);
|
||
padding: 22px 24px 24px;
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
/* A rule under the heading gives each panel a real header band instead of
|
||
a title floating above loose content — the cheapest structural cue there
|
||
is, and what most separates "designed" from "stacked divs". Suppressed
|
||
inside modals, which set their own tighter header treatment below. */
|
||
.panel-head {
|
||
display: flex; align-items: baseline; justify-content: space-between;
|
||
gap: 12px; flex-wrap: wrap;
|
||
padding-bottom: 13px; margin-bottom: 4px;
|
||
border-bottom: 1px solid var(--hairline);
|
||
}
|
||
.hint { font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
||
.search-status {
|
||
margin-top: 14px; font-size: 13.5px; color: var(--ink-2);
|
||
padding: 11px 13px; border-radius: 10px;
|
||
background: var(--surface-2); border: 1px solid var(--hairline);
|
||
}
|
||
|
||
/* --------------------------------------------------------------- table */
|
||
|
||
.table-scroll { overflow-x: auto; margin: 0 -6px; padding: 0 6px; }
|
||
.grid { width: 100%; border-collapse: collapse; font-size: 14.5px; }
|
||
.grid th {
|
||
text-align: left; font-size: 11px; font-weight: 700;
|
||
text-transform: uppercase; letter-spacing: .07em; color: var(--muted);
|
||
padding: 10px 12px; border-bottom: 1px solid var(--rule); white-space: nowrap;
|
||
}
|
||
.grid td {
|
||
padding: 13px 12px; border-bottom: 1px solid var(--hairline);
|
||
vertical-align: middle;
|
||
}
|
||
.grid tbody tr { cursor: pointer; transition: background .12s ease; }
|
||
.grid tbody tr:hover { background: var(--surface-2); }
|
||
.grid tbody tr:last-child td { border-bottom: none; }
|
||
.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||
|
||
.cardcell { display: flex; align-items: center; gap: 13px; }
|
||
.cardcell img, .cardcell .thumb-blank {
|
||
width: 46px; height: 64px; object-fit: cover;
|
||
border-radius: 6px; background: var(--surface-2); flex: none;
|
||
box-shadow: 0 1px 3px rgba(16, 20, 28, .18), inset 0 0 0 1px rgba(16, 20, 28, .08);
|
||
}
|
||
.thumb-blank {
|
||
display: inline-block;
|
||
border: 1px dashed var(--rule);
|
||
background:
|
||
repeating-linear-gradient(135deg, transparent 0 5px, var(--hairline) 5px 6px),
|
||
var(--surface-2);
|
||
}
|
||
.cardcell-name { font-weight: 600; line-height: 1.3; }
|
||
.cardcell-meta { font-size: 12.5px; color: var(--muted); }
|
||
.dash { color: var(--muted); }
|
||
|
||
/* Table refinements the density audit turned up: dates were wrapping onto
|
||
two lines, action buttons were stacking vertically in a cramped cell, and
|
||
confidence read as one more grey word. */
|
||
.cell-when { white-space: nowrap; font-variant-numeric: tabular-nums; }
|
||
.cell-conf {
|
||
font-size: 11px; font-weight: 700; letter-spacing: .08em;
|
||
text-transform: uppercase;
|
||
}
|
||
.cell-conf.conf-high { color: var(--good-text); }
|
||
.cell-conf.conf-medium { color: var(--warning); }
|
||
.cell-conf.conf-low { color: var(--muted); }
|
||
.row-actions {
|
||
display: flex; gap: 7px; justify-content: flex-end;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* --------------------------------------------------------------- pills */
|
||
|
||
.pill {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
font-size: 12px; font-weight: 650;
|
||
padding: 4.5px 11px; border-radius: 99px;
|
||
border: 1px solid var(--rule); color: var(--ink-2);
|
||
white-space: nowrap;
|
||
}
|
||
.pill-grade { border-color: color-mix(in srgb, var(--good) 55%, transparent);
|
||
color: var(--good-text);
|
||
background: color-mix(in srgb, var(--good) 15%, transparent); }
|
||
.pill-marginal { border-color: color-mix(in srgb, var(--warning) 55%, transparent);
|
||
color: var(--warning);
|
||
background: color-mix(in srgb, var(--warning) 15%, transparent); }
|
||
.pill-raw { border-color: var(--rule); color: var(--muted); }
|
||
.pill-critical { border-color: color-mix(in srgb, var(--critical) 55%, transparent);
|
||
color: var(--critical);
|
||
background: color-mix(in srgb, var(--critical) 15%, transparent); }
|
||
|
||
/* Empty state. The old version was a one-line shrug; this gives the panel
|
||
something to hold and says what will show up here. */
|
||
.empty {
|
||
text-align: center; color: var(--muted); font-size: 14px;
|
||
padding: 40px 20px 34px;
|
||
}
|
||
.empty::before {
|
||
content: ''; display: block; width: 42px; height: 58px; margin: 0 auto 14px;
|
||
border-radius: 6px; border: 1.5px dashed var(--rule);
|
||
background:
|
||
repeating-linear-gradient(135deg, transparent 0 6px, var(--hairline) 6px 7px),
|
||
var(--surface-2);
|
||
opacity: .8;
|
||
}
|
||
|
||
/* -------------------------------------------------------------- modal */
|
||
|
||
.scrim {
|
||
position: fixed; inset: 0; z-index: 30;
|
||
background: rgba(16, 20, 28, .42);
|
||
-webkit-backdrop-filter: blur(3px);
|
||
backdrop-filter: blur(3px);
|
||
}
|
||
.modal {
|
||
position: fixed; z-index: 40;
|
||
top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||
width: min(600px, calc(100vw - 28px));
|
||
max-height: min(88vh, 940px); overflow-y: auto;
|
||
background: var(--surface-1);
|
||
border: 1px solid var(--rule);
|
||
border-radius: var(--radius-lg);
|
||
box-shadow: var(--shadow-lg);
|
||
padding: 24px 26px 26px;
|
||
}
|
||
/* The modal's heading is a card name and can run long. The shared
|
||
.panel-head wraps (which is right on the main panels, where a short title
|
||
sits beside a hint), but here wrapping drops the close button onto its own
|
||
line under the title. Pin it: title takes the slack, × stays top-right. */
|
||
.modal .panel-head {
|
||
flex-wrap: nowrap; align-items: flex-start; gap: 16px;
|
||
padding-bottom: 0; margin-bottom: 12px; border-bottom: none;
|
||
}
|
||
.modal .panel-head h2 { flex: 1; min-width: 0; }
|
||
.modal .close { flex: none; margin-top: 1px; }
|
||
.close {
|
||
background: none; border: none; cursor: pointer; font-size: 24px;
|
||
color: var(--muted); line-height: 1; padding: 0 4px; border-radius: 6px;
|
||
}
|
||
.close:hover { color: var(--ink); }
|
||
|
||
.section { margin-bottom: 24px; }
|
||
.section > h3 { margin-bottom: 12px; }
|
||
.fields { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||
.field { display: flex; flex-direction: column; gap: 6px; }
|
||
.field.wide { grid-column: 1 / -1; }
|
||
.field label { font-size: 12.5px; color: var(--ink-2); font-weight: 550; }
|
||
.suffix { display: block; font-size: 12px; line-height: 1.5; color: var(--muted); }
|
||
|
||
.drawer-actions {
|
||
display: flex; gap: 9px; flex-wrap: wrap; align-items: center;
|
||
padding-top: 18px; margin-top: 18px; border-top: 1px solid var(--hairline);
|
||
}
|
||
.spacer { flex: 1; }
|
||
|
||
/* Informational by default — accent, not amber. The shooting guidance
|
||
("front straight-on is the minimum") is instruction, not a caution, and
|
||
colouring it like a warning spent the page's only alarm colour on
|
||
something that never needed it. Genuine cautions opt into .note-warn. */
|
||
.note {
|
||
font-size: 13px; line-height: 1.55; color: var(--ink-2);
|
||
background: var(--surface-2); border: 1px solid var(--hairline);
|
||
/* Navy, not the brand red: this box is instruction, and now that red is
|
||
the brand colour an informational red bar would read as an alert. */
|
||
border-left: 3px solid var(--accent-2);
|
||
border-radius: 8px; padding: 12px 14px; margin-top: 14px;
|
||
}
|
||
.note-warn { border-left-color: var(--warning); }
|
||
.note-flag {
|
||
border-left-color: var(--critical);
|
||
color: var(--critical);
|
||
margin-top: 0; margin-bottom: 14px;
|
||
}
|
||
|
||
/* --------------------------------------------------------- authenticity */
|
||
/* A concern about whether the card is what it claims to be, not its
|
||
condition — deliberately styled as a full banner rather than a hint line
|
||
like every other measurement caveat, so it reads as "stop and look at
|
||
this" before anything about the grade itself. Two tiers, both using
|
||
--critical rather than --brand: this is exactly the "distrust this"
|
||
signal that colour is reserved for elsewhere in the app, and the brand
|
||
red would blur into decoration here of all places. */
|
||
.authenticity-banner {
|
||
border: 1px solid color-mix(in srgb, var(--critical) 45%, transparent);
|
||
background: color-mix(in srgb, var(--critical) 10%, var(--surface-1));
|
||
border-radius: 8px;
|
||
padding: 11px 14px;
|
||
margin-bottom: 14px;
|
||
}
|
||
.authenticity-check {
|
||
border-color: color-mix(in srgb, var(--warning) 55%, transparent);
|
||
background: color-mix(in srgb, var(--warning) 12%, var(--surface-1));
|
||
}
|
||
.authenticity-title {
|
||
font-weight: 700; font-size: 13.5px;
|
||
color: var(--critical);
|
||
}
|
||
.authenticity-check .authenticity-title { color: var(--warning); }
|
||
.authenticity-detail {
|
||
font-size: 12.5px; color: var(--ink-2); margin-top: 3px; line-height: 1.5;
|
||
}
|
||
|
||
/* History-row version: a plain dot, since the row has no room for the full
|
||
banner text and opening the card gives the real explanation. */
|
||
.authenticity-dot {
|
||
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
|
||
margin-right: 6px; vertical-align: middle;
|
||
background: var(--critical);
|
||
}
|
||
.authenticity-dot.authenticity-check { background: var(--warning); }
|
||
|
||
/* -------------------------------------------------------------- banner */
|
||
|
||
.banner {
|
||
padding: 12px 24px; font-size: 13.5px;
|
||
background: var(--brand-soft);
|
||
color: var(--ink);
|
||
border-bottom: 1px solid var(--rule);
|
||
box-shadow: inset 3px 0 0 var(--brand);
|
||
}
|
||
.banner.err {
|
||
background: var(--critical);
|
||
color: #fff;
|
||
box-shadow: none;
|
||
}
|
||
.banner ul { margin: 5px 0 0; padding-left: 18px; }
|
||
|
||
/* ---------------------------------------------------------- grade slab */
|
||
/* Styled after the label on a graded slab, because that's the thing this
|
||
whole app is estimating — it makes the number read as a verdict rather
|
||
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: 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::before {
|
||
content: ''; position: absolute; left: 0; right: 0; top: 0; height: 3px;
|
||
background: var(--slab-red);
|
||
}
|
||
.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: 136px;
|
||
display: flex; flex-direction: column; align-items: center; justify-content: 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 {
|
||
font-size: 58px; font-weight: 800; line-height: .95; letter-spacing: -.035em;
|
||
font-variant-numeric: tabular-nums;
|
||
text-shadow: 0 2px 10px rgba(0, 0, 0, .4);
|
||
}
|
||
.slab-grade .word {
|
||
font-size: 10px; font-weight: 800; letter-spacing: .13em;
|
||
text-transform: uppercase; opacity: .9; margin-top: 7px;
|
||
}
|
||
|
||
/* Grade bands. Green for a card worth submitting, blue upper-mid, amber mid,
|
||
red low — the same red reserved elsewhere for "distrust this", used here
|
||
because a low grade is genuinely the bad outcome. */
|
||
.slab-10 .slab-grade, .slab-9 .slab-grade { background: linear-gradient(150deg, #0b6d33, #16a34a); }
|
||
.slab-8 .slab-grade, .slab-7 .slab-grade { background: linear-gradient(150deg, #1b4f8f, #2f6fd0); }
|
||
.slab-6 .slab-grade, .slab-5 .slab-grade { background: linear-gradient(150deg, #9a5c0b, #e0891f); }
|
||
.slab-low .slab-grade { background: linear-gradient(150deg, #8f1f24, #d13a40); }
|
||
|
||
/* A 10 gets the foil treatment: a full holo sweep plus a slow shimmer pass,
|
||
the way a refractor catches light as you tilt it. Deliberately reserved
|
||
for the top grade so it stays meaningful. */
|
||
.slab-10 .slab-grade {
|
||
background: linear-gradient(140deg, #0b6d33, #3ddcc0 38%, #4d9bff 66%, #a97bff);
|
||
}
|
||
.slab-10 .slab-grade::after {
|
||
content: ''; position: absolute; inset: -60% -20%;
|
||
background: linear-gradient(72deg,
|
||
transparent 38%, rgba(255, 255, 255, .5) 50%, transparent 62%);
|
||
animation: foil-sweep 3.6s ease-in-out infinite;
|
||
}
|
||
@keyframes foil-sweep {
|
||
0%, 62% { transform: translateX(-115%); }
|
||
100% { transform: translateX(115%); }
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.slab-10 .slab-grade::after { animation: none; opacity: .25; }
|
||
}
|
||
|
||
/* 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 */
|
||
|
||
@media (max-width: 700px) {
|
||
body { font-size: 15.5px; }
|
||
main { padding: 18px 13px 72px; gap: 16px; }
|
||
.topbar { padding: 11px 14px; }
|
||
.panel { padding: 18px 16px 20px; border-radius: var(--radius); }
|
||
h2 { font-size: 19px; }
|
||
.fields { grid-template-columns: 1fr; }
|
||
.grid { font-size: 14px; }
|
||
.grid td { padding: 11px 8px; }
|
||
.grid th { padding: 9px 8px; }
|
||
.cardcell img, .cardcell .thumb-blank { width: 40px; height: 56px; }
|
||
.banner { padding: 11px 14px; }
|
||
|
||
/* 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: 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
|
||
scrolling. Confidence is the most expendable column there — the grade
|
||
pill and the card name are what someone is actually scanning for, and
|
||
confidence is still shown in the card's detail view. */
|
||
@media (max-width: 560px) {
|
||
#history-table th:nth-child(3),
|
||
#history-table td:nth-child(3) { display: none; }
|
||
}
|