card-grader/static/style.css

576 lines
22 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 — "collector-grade" dark theme.
Card-type agnostic (Pokemon, sports, TCG).
Committed dark, not a dark *mode*. The light palette was removed rather
than kept as a fallback: this is one deliberate look, and a half-tended
second palette is how a design drifts out of sync with itself. Phones in
light mode get this same design, which is the intent.
Colour discipline (carried over from the light theme, still true):
- Grade severities ride a lightness ladder (none -> minor -> moderate ->
major), so the table still reads as escalating severity in greyscale.
- Red is reserved for "you should distrust this number" (unmeasurable
edges, a low grade) — never decorative.
- Foil/holo treatment is reserved for a 10. Every card shimmering would
say nothing. */
:root {
color-scheme: dark;
/* Ground is nearly black with a blue cast — a neutral grey ground makes
the card thumbnails look washed, since most slabs and holos are cool. */
--ground: #06090f;
--surface-1: #0e1522;
--surface-2: #141d2e; /* raised: modal, slab body, hover */
--ink: #eef3fb;
--ink-2: #aab8cf;
--muted: #7e8da6;
--hairline: #1c2740;
--rule: #2a3852;
--accent: #4d9bff;
--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;
--critical: #f2555a;
--ring: rgba(255, 255, 255, .12);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, .5);
--shadow: 0 2px 6px rgba(0, 0, 0, .5), 0 16px 40px rgba(0, 0, 0, .55);
--shadow-lg: 0 4px 12px rgba(0, 0, 0, .55), 0 28px 70px rgba(0, 0, 0, .65);
--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;
/* Two broad, very low-opacity glows instead of the old dot grid. A tiled
pattern reads as "utility dashboard"; soft light on a dark ground reads
as depth. Fixed so it doesn't slide around under the content. */
background-color: var(--ground);
background-image:
radial-gradient(900px 600px at 12% -8%, rgba(77, 155, 255, .10), transparent 60%),
radial-gradient(760px 520px at 92% 4%, rgba(61, 220, 192, .07), transparent 62%);
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(8, 12, 20, .82);
-webkit-backdrop-filter: blur(14px) saturate(1.4);
backdrop-filter: blur(14px) saturate(1.4);
border-bottom: 1px solid var(--hairline);
}
/* 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: 1.5px;
background: linear-gradient(90deg,
#ff5f8f, #ffb347, #ffe66d, #3ddcc0, #4d9bff, #a97bff, #ff5f8f);
opacity: .5;
}
.topbar h1 {
color: #fff; font-size: 16px; font-weight: 600; letter-spacing: -.005em;
}
.topbar .btn {
background: var(--accent-2); color: #04231d;
border-color: transparent; font-weight: 650;
}
.topbar .btn:hover { filter: brightness(1.08); }
.topbar .btn-quiet {
background: rgba(255, 255, 255, .07); color: var(--ink);
border-color: rgba(255, 255, 255, .16); font-weight: 550;
}
.topbar .btn-quiet:hover { background: rgba(255, 255, 255, .13); 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; }
.brand-mark .mark-case { stroke: var(--accent); }
.brand-mark .mark-label { fill: var(--accent-2); }
/* .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); 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(--accent);
color: #04121f;
border: 1px solid transparent;
text-decoration: none; display: inline-block; line-height: 1.4;
white-space: nowrap;
box-shadow: 0 1px 2px rgba(0, 0, 0, .35);
transition: filter .13s ease, transform .13s ease, background .13s ease;
}
.btn:hover { filter: brightness(1.08); }
.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-2); color: var(--ink);
border-color: var(--rule); box-shadow: var(--shadow-sm);
}
.btn-quiet:hover { background: #1a2439; 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: #0a1120;
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(--accent);
box-shadow: 0 0 0 3px var(--accent-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 */
.panel {
position: relative;
background:
linear-gradient(180deg, rgba(255, 255, 255, .035), transparent 120px),
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 2px 8px rgba(0, 0, 0, .55), inset 0 0 0 1px rgba(255, 255, 255, .07);
}
.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(3, 5, 10, .68);
-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:
linear-gradient(180deg, rgba(255, 255, 255, .04), transparent 140px),
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);
border-left: 3px solid var(--accent);
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: color-mix(in srgb, var(--accent) 22%, var(--ground));
color: var(--ink);
border-bottom: 1px solid var(--hairline);
}
.banner.err {
background: color-mix(in srgb, var(--critical) 82%, #000);
color: #fff;
}
.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; }
}