Full PSA palette: light silver ground, brand red accent, dark text

This commit is contained in:
Barely Removable 2026-08-22 12:41:53 -07:00
parent 2abb568c49
commit f8db3df605
4 changed files with 104 additions and 92 deletions

View file

@ -1,9 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<!-- Matches the in-app brand mark: a graded slab — case, label, and the <!-- Matches the in-app brand mark: a graded slab — case, label, and the
card sealed inside. Kept in step with .brand-mark in style.css. --> card sealed inside. Kept in step with .brand-mark in style.css.
<rect width="128" height="128" rx="26" fill="#0e1522"/> White field with the brand red label band, per the light theme. -->
<rect width="128" height="128" rx="26" fill="#ffffff"/>
<rect x="0.5" y="0.5" width="127" height="127" rx="25.5" fill="none"
stroke="#ccd2dc" stroke-width="1"/>
<rect x="41.5" y="18.5" width="45" height="91" rx="7.5" fill="none" <rect x="41.5" y="18.5" width="45" height="91" rx="7.5" fill="none"
stroke="#4d9bff" stroke-width="5"/> stroke="#1c2b45" stroke-width="5"/>
<rect x="50" y="30" width="28" height="8" rx="3" fill="#3ddcc0"/> <rect x="50" y="30" width="28" height="8" rx="3" fill="#c8102e"/>
<rect x="50" y="49" width="28" height="46" rx="3.5" fill="#4d9bff" opacity="0.48"/> <rect x="50" y="49" width="28" height="46" rx="3.5" fill="#1c2b45" opacity="0.48"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 548 B

After

Width:  |  Height:  |  Size: 734 B

Before After
Before After

View file

@ -6,13 +6,13 @@
<title>Card Grader</title> <title>Card Grader</title>
<link rel="manifest" href="__BASE__/static/manifest.json"> <link rel="manifest" href="__BASE__/static/manifest.json">
<link rel="icon" href="__BASE__/static/icon.svg"> <link rel="icon" href="__BASE__/static/icon.svg">
<meta name="theme-color" content="#16224a"> <meta name="theme-color" content="#ffffff">
<!-- iOS ignores the web app manifest for install behaviour and needs its own <!-- iOS ignores the web app manifest for install behaviour and needs its own
meta tags. Without these, "Add to Home Screen" on an iPhone produces a meta tags. Without these, "Add to Home Screen" on an iPhone produces a
bookmark that opens in Safari with the address bar showing, rather than bookmark that opens in Safari with the address bar showing, rather than
something that looks and behaves like an installed app. --> something that looks and behaves like an installed app. -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Card Grader"> <meta name="apple-mobile-web-app-title" content="Card Grader">
<link rel="apple-touch-icon" href="__BASE__/static/icon-180.png"> <link rel="apple-touch-icon" href="__BASE__/static/icon-180.png">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">

View file

@ -6,8 +6,8 @@
"scope": "__BASE__/", "scope": "__BASE__/",
"display": "standalone", "display": "standalone",
"orientation": "portrait", "orientation": "portrait",
"background_color": "#0a0f1c", "background_color": "#eef0f4",
"theme_color": "#16224a", "theme_color": "#ffffff",
"icons": [ "icons": [
{ "src": "__BASE__/static/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { "src": "__BASE__/static/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
{ "src": "__BASE__/static/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { "src": "__BASE__/static/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },

View file

@ -1,56 +1,64 @@
/* Card Grader "collector-grade" dark theme. /* Card Grader slab-house theme: silver ground, red accent, black text.
Card-type agnostic (Pokemon, sports, TCG). Card-type agnostic (Pokemon, sports, TCG).
Committed dark, not a dark *mode*. The light palette was removed rather Committed light, not a light *mode*. A single deliberate look; a
than kept as a fallback: this is one deliberate look, and a half-tended half-tended second palette is how a design drifts out of sync with
second palette is how a design drifts out of sync with itself. Phones in itself. Phones in dark mode get this same design, which is the intent.
light mode get this same design, which is the intent.
Colour discipline (carried over from the light theme, still true): Colour discipline:
- Grade severities ride a lightness ladder (none -> minor -> moderate -> - Grade severities ride a lightness ladder (none -> minor -> moderate ->
major), so the table still reads as escalating severity in greyscale. major), so the table still reads as escalating severity in greyscale.
- Red is reserved for "you should distrust this number" (unmeasurable - Red is the BRAND here, so it can no longer also mean "distrust this
edges, a low grade) never decorative. 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 - Foil/holo treatment is reserved for a 10. Every card shimmering would
say nothing. */ say nothing. */
:root { :root {
color-scheme: dark; color-scheme: light;
/* Ground is nearly black with a blue cast a neutral grey ground makes /* Ground is a faint cool grey rather than pure white slab labels, card
the card thumbnails look washed, since most slabs and holos are cool. */ thumbnails and the panels all sit ON something, and pure white leaves
--ground: #06090f; nothing for a white label to separate from. */
--surface-1: #0e1522; --ground: #eef0f4;
--surface-2: #141d2e; /* raised: modal, slab body, hover */ --surface-1: #ffffff;
--ink: #eef3fb; --surface-2: #f4f6f9; /* raised: modal insets, slab body, hover */
--ink-2: #aab8cf; --ink: #10141c;
--muted: #7e8da6; --ink-2: #47505f;
--hairline: #1c2740; --muted: #6b7484;
--rule: #2a3852; --hairline: #e2e6ec;
--rule: #ccd2dc;
--accent: #4d9bff; /* PSA-style red. Used for brand chrome and primary actions. */
--accent-2: #3ddcc0; --brand: #c8102e;
--accent-soft: rgba(77, 155, 255, .14); --brand-dark: #a20d25;
--brand-soft: rgba(200, 16, 46, .09);
/* Label chrome only the red band on a slab label, never a severity. --accent: var(--brand);
Deliberately deeper than --critical (#f2555a) so the two don't read as --accent-2: #1c2b45; /* deep navy — the label's text/secondary tone */
the same signal: this one is decoration on a physical-looking object, --accent-soft: var(--brand-soft);
that one means "distrust this number". */
--slab-red: #c8102e; /* Slab label tokens. On a light ground the label is near-white and
--slab-face: #eceef2; 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: #0c111a;
--slab-ink-2: #5a6474; --slab-ink-2: #5a6474;
--slab-rule: #b9c0cc; --slab-rule: #c2c8d2;
--good: #23c55e; --good: #16853f;
--good-text: #4ade80; --good-text: #127034;
--warning: #f5a33c; --warning: #a86200;
--critical: #f2555a; /* Deliberately darker and browner than --brand so a severity red never
reads as the brand red sitting next to it. */
--critical: #8f1d1d;
--ring: rgba(255, 255, 255, .12); --ring: rgba(16, 20, 28, .12);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, .5); --shadow-sm: 0 1px 2px rgba(16, 20, 28, .08);
--shadow: 0 2px 6px rgba(0, 0, 0, .5), 0 16px 40px rgba(0, 0, 0, .55); --shadow: 0 1px 2px rgba(16, 20, 28, .06), 0 6px 18px rgba(16, 20, 28, .08);
--shadow-lg: 0 4px 12px rgba(0, 0, 0, .55), 0 28px 70px rgba(0, 0, 0, .65); --shadow-lg: 0 2px 6px rgba(16, 20, 28, .08), 0 18px 44px rgba(16, 20, 28, .14);
--radius: 14px; --radius: 14px;
--radius-lg: 18px; --radius-lg: 18px;
@ -65,13 +73,13 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
/* Two broad, very low-opacity glows instead of the old dot grid. A tiled /* A single very faint warm-red wash at the top, so the ground isn't a
pattern reads as "utility dashboard"; soft light on a dark ground reads flat slab of grey but doesn't compete with the content either. The
as depth. Fixed so it doesn't slide around under the content. */ 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-color: var(--ground);
background-image: background-image:
radial-gradient(900px 600px at 12% -8%, rgba(77, 155, 255, .10), transparent 60%), radial-gradient(1100px 520px at 50% -12%, rgba(200, 16, 46, .06), transparent 70%);
radial-gradient(760px 520px at 92% 4%, rgba(61, 220, 192, .07), transparent 62%);
background-attachment: fixed; background-attachment: fixed;
background-repeat: no-repeat; background-repeat: no-repeat;
min-height: 100vh; min-height: 100vh;
@ -105,10 +113,10 @@ main {
.topbar { .topbar {
position: relative; /* containing block for the holo edge below */ position: relative; /* containing block for the holo edge below */
background: rgba(8, 12, 20, .82); background: rgba(255, 255, 255, .88);
-webkit-backdrop-filter: blur(14px) saturate(1.4); -webkit-backdrop-filter: blur(14px) saturate(1.3);
backdrop-filter: blur(14px) saturate(1.4); backdrop-filter: blur(14px) saturate(1.3);
border-bottom: 1px solid var(--hairline); border-bottom: 1px solid var(--rule);
} }
/* The bar spans the viewport, but its CONTENTS line up with the page /* 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 column the logo sitting hard against a 3440px screen edge while the
@ -125,25 +133,20 @@ main {
number lives but dialled back from the original full-strength stripe, number lives but dialled back from the original full-strength stripe,
which was the loudest thing on an otherwise restrained page. */ which was the loudest thing on an otherwise restrained page. */
.topbar::after { .topbar::after {
content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 1.5px; content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px;
background: linear-gradient(90deg, background: linear-gradient(90deg,
#ff5f8f, #ffb347, #ffe66d, #3ddcc0, #4d9bff, #a97bff, #ff5f8f); #ff5f8f, #ffb347, #ffe66d, #3ddcc0, #4d9bff, #a97bff, #ff5f8f);
opacity: .5; opacity: .8; /* light ground needs more of it than dark did to register */
} }
.topbar h1 { .topbar h1 {
color: #fff; font-size: 16px; font-weight: 600; letter-spacing: -.005em; color: var(--ink); font-size: 16px; font-weight: 700; letter-spacing: -.005em;
} }
.topbar .btn { /* The header's buttons inherit the page's button styling now. On the dark
background: var(--accent-2); color: #04231d; theme they needed their own palette to stay legible against the bar;
border-color: transparent; font-weight: 650; against a white bar they don't, and overriding them only made the header
} inconsistent with every other control. */
.topbar .btn:hover { filter: brightness(1.08); } .topbar .btn-quiet:hover { background: var(--surface-2); filter: none; }
.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 { display: flex; align-items: center; gap: 11px; }
@ -152,12 +155,15 @@ main {
checkmark it replaced: that read as a generic app badge and said nothing 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. */ about grading. Thin strokes and no glow keep it legible at 22px. */
.brand-mark { flex: none; width: 18px; height: 27px; display: block; } .brand-mark { flex: none; width: 18px; height: 27px; display: block; }
.brand-mark .mark-case { stroke: var(--accent); } /* Case in the label's navy, label band in brand red the same two-tone
.brand-mark .mark-label { fill: var(--accent-2); } 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 /* .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 stops separating from the case interior at topbar size and the mark
collapses into an empty outline. Checked by rendering it at 42px. */ collapses into an empty outline. Checked by rendering it at 42px. */
.brand-mark .mark-card { fill: var(--accent); opacity: .48; } .brand-mark .mark-card { fill: var(--accent-2); opacity: .48; }
.topbar-actions { display: flex; gap: 9px; flex-wrap: wrap; } .topbar-actions { display: flex; gap: 9px; flex-wrap: wrap; }
@ -170,15 +176,15 @@ main {
.btn { .btn {
font: inherit; font-weight: 600; font-size: 14.5px; font: inherit; font-weight: 600; font-size: 14.5px;
padding: 9px 17px; border-radius: 9px; cursor: pointer; padding: 9px 17px; border-radius: 9px; cursor: pointer;
background: var(--accent); background: var(--brand);
color: #04121f; color: #fff;
border: 1px solid transparent; border: 1px solid transparent;
text-decoration: none; display: inline-block; line-height: 1.4; text-decoration: none; display: inline-block; line-height: 1.4;
white-space: nowrap; white-space: nowrap;
box-shadow: 0 1px 2px rgba(0, 0, 0, .35); box-shadow: var(--shadow-sm);
transition: filter .13s ease, transform .13s ease, background .13s ease; transition: filter .13s ease, transform .13s ease, background .13s ease;
} }
.btn:hover { filter: brightness(1.08); } .btn:hover { background: var(--brand-dark); filter: none; }
.btn:active { transform: translateY(1px); } .btn:active { transform: translateY(1px); }
.btn[disabled] { opacity: .45; cursor: default; filter: none; box-shadow: none; } .btn[disabled] { opacity: .45; cursor: default; filter: none; box-shadow: none; }
.btn:focus-visible, .close:focus-visible { .btn:focus-visible, .close:focus-visible {
@ -186,10 +192,10 @@ main {
} }
.btn-quiet { .btn-quiet {
background: var(--surface-2); color: var(--ink); background: var(--surface-1); color: var(--ink);
border-color: var(--rule); box-shadow: var(--shadow-sm); border-color: var(--rule); box-shadow: var(--shadow-sm);
} }
.btn-quiet:hover { background: #1a2439; filter: none; } .btn-quiet:hover { background: var(--surface-2); filter: none; }
.btn-danger { .btn-danger {
background: transparent; color: var(--critical); background: transparent; color: var(--critical);
@ -205,7 +211,7 @@ main {
padding: 9px 12px; padding: 9px 12px;
border: 1px solid var(--rule); border: 1px solid var(--rule);
border-radius: 10px; border-radius: 10px;
background: #0a1120; background: var(--surface-1);
color: var(--ink); color: var(--ink);
width: 100%; width: 100%;
transition: border-color .13s ease, box-shadow .13s ease; transition: border-color .13s ease, box-shadow .13s ease;
@ -213,8 +219,8 @@ main {
.input::placeholder, textarea::placeholder { color: var(--muted); } .input::placeholder, textarea::placeholder { color: var(--muted); }
.input:focus, select:focus, textarea:focus { .input:focus, select:focus, textarea:focus {
outline: none; outline: none;
border-color: var(--accent); border-color: var(--brand);
box-shadow: 0 0 0 3px var(--accent-soft); box-shadow: 0 0 0 3px var(--brand-soft);
} }
textarea { resize: vertical; min-height: 64px; } textarea { resize: vertical; min-height: 64px; }
@ -227,11 +233,12 @@ textarea { resize: vertical; min-height: 64px; }
/* -------------------------------------------------------------- panels */ /* -------------------------------------------------------------- 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 { .panel {
position: relative; position: relative;
background: background: var(--surface-1);
linear-gradient(180deg, rgba(255, 255, 255, .035), transparent 120px),
var(--surface-1);
border: 1px solid var(--hairline); border: 1px solid var(--hairline);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
padding: 22px 24px 24px; padding: 22px 24px 24px;
@ -277,7 +284,7 @@ textarea { resize: vertical; min-height: 64px; }
.cardcell img, .cardcell .thumb-blank { .cardcell img, .cardcell .thumb-blank {
width: 46px; height: 64px; object-fit: cover; width: 46px; height: 64px; object-fit: cover;
border-radius: 6px; background: var(--surface-2); flex: none; 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); box-shadow: 0 1px 3px rgba(16, 20, 28, .18), inset 0 0 0 1px rgba(16, 20, 28, .08);
} }
.thumb-blank { .thumb-blank {
display: inline-block; display: inline-block;
@ -345,7 +352,7 @@ textarea { resize: vertical; min-height: 64px; }
.scrim { .scrim {
position: fixed; inset: 0; z-index: 30; position: fixed; inset: 0; z-index: 30;
background: rgba(3, 5, 10, .68); background: rgba(16, 20, 28, .42);
-webkit-backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
backdrop-filter: blur(3px); backdrop-filter: blur(3px);
} }
@ -354,9 +361,7 @@ textarea { resize: vertical; min-height: 64px; }
top: 50%; left: 50%; transform: translate(-50%, -50%); top: 50%; left: 50%; transform: translate(-50%, -50%);
width: min(600px, calc(100vw - 28px)); width: min(600px, calc(100vw - 28px));
max-height: min(88vh, 940px); overflow-y: auto; max-height: min(88vh, 940px); overflow-y: auto;
background: background: var(--surface-1);
linear-gradient(180deg, rgba(255, 255, 255, .04), transparent 140px),
var(--surface-1);
border: 1px solid var(--rule); border: 1px solid var(--rule);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
@ -399,7 +404,9 @@ textarea { resize: vertical; min-height: 64px; }
.note { .note {
font-size: 13px; line-height: 1.55; color: var(--ink-2); font-size: 13px; line-height: 1.55; color: var(--ink-2);
background: var(--surface-2); border: 1px solid var(--hairline); background: var(--surface-2); border: 1px solid var(--hairline);
border-left: 3px solid var(--accent); /* 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; border-radius: 8px; padding: 12px 14px; margin-top: 14px;
} }
.note-warn { border-left-color: var(--warning); } .note-warn { border-left-color: var(--warning); }
@ -413,13 +420,15 @@ textarea { resize: vertical; min-height: 64px; }
.banner { .banner {
padding: 12px 24px; font-size: 13.5px; padding: 12px 24px; font-size: 13.5px;
background: color-mix(in srgb, var(--accent) 22%, var(--ground)); background: var(--brand-soft);
color: var(--ink); color: var(--ink);
border-bottom: 1px solid var(--hairline); border-bottom: 1px solid var(--rule);
box-shadow: inset 3px 0 0 var(--brand);
} }
.banner.err { .banner.err {
background: color-mix(in srgb, var(--critical) 82%, #000); background: var(--critical);
color: #fff; color: #fff;
box-shadow: none;
} }
.banner ul { margin: 5px 0 0; padding-left: 18px; } .banner ul { margin: 5px 0 0; padding-left: 18px; }