card-grader/static/style.css

585 lines
23 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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;
--good-text: #127034;
--warning: #a86200;
/* 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;
}
/* -------------------------------------------------------------- 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; }
}