card-grader/static/style.css

498 lines
18 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);
--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 */
.topbar {
position: sticky; top: 0; z-index: 20;
display: flex; align-items: center; justify-content: space-between;
gap: 16px; flex-wrap: wrap;
padding: 14px 24px;
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);
}
/* 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. Kept from the light theme — it's the app's signature. */
.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: .9;
}
.topbar h1 { color: #fff; 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: 12px; }
/* Brand mark: a graded slab, drawn in plain CSS. A rounded card outline with
a check — "this one's been looked at" — rather than any single game's
iconography, since this grades anything PSA does. */
.brand-mark {
position: relative; flex: none;
width: 30px; height: 30px; border-radius: 8px;
background: linear-gradient(150deg, var(--accent-2), var(--accent) 70%, #a97bff);
box-shadow:
inset 0 0 0 1.5px rgba(255, 255, 255, .34),
0 2px 10px rgba(77, 155, 255, .45);
}
.brand-mark::after {
content: ''; position: absolute; left: 8px; top: 8px;
width: 9px; height: 5.5px;
border-left: 2.2px solid #04231d; border-bottom: 2.2px solid #04231d;
transform: rotate(-45deg);
}
.topbar-actions { display: flex; gap: 9px; flex-wrap: wrap; }
/* ------------------------------------------------------------ controls */
.btn {
font: inherit; font-weight: 550; font-size: 14.5px;
padding: 9px 17px; border-radius: 10px; cursor: pointer;
background: linear-gradient(180deg,
color-mix(in srgb, var(--accent) 92%, #fff),
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, .4), 0 4px 14px rgba(77, 155, 255, .22);
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);
}
.panel-head {
display: flex; align-items: baseline; justify-content: space-between;
gap: 12px; flex-wrap: wrap; margin-bottom: 6px;
}
.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;
margin-bottom: 14px;
}
.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; }
.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(--warning);
border-radius: 8px; padding: 12px 14px; margin-top: 14px;
}
.note-flag {
border-left-color: var(--critical);
color: var(--critical);
margin-top: 0; margin-bottom: 14px;
}
/* -------------------------------------------------------------- banner */
.banner {
position: sticky; top: 60px; z-index: 15;
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. */
.slab {
display: flex; align-items: stretch; gap: 0;
border-radius: var(--radius); overflow: hidden;
border: 1px solid var(--rule);
background: var(--surface-2);
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-title { font-size: 17px; font-weight: 700; line-height: 1.3; letter-spacing: -.01em; }
.slab-sub { font-size: 12.5px; color: var(--muted); }
.slab-grade {
position: relative; overflow: hidden;
flex: none; width: 148px;
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 20px 10px; color: #fff; text-align: center;
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-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);
}
/* --------------------------------------------------------------- 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 { top: 54px; 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: 15px 16px; }
.slab-title { font-size: 15.5px; }
.slab-grade { width: 108px; padding: 16px 8px; }
.slab-grade .n { font-size: 44px; }
}
/* 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; }
}