diff --git a/src/App.tsx b/src/App.tsx
index c695886..552f004 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -325,9 +325,10 @@ export default function App() {
key={n.id}
className={`nav-item ${tab === n.id ? "active" : ""}`}
onClick={() => setTab(n.id)}
+ title={n.label}
>
{n.icon}
- {n.label}
+ {n.label}
))}
diff --git a/src/styles.css b/src/styles.css
index 9ccf684..c5261d5 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -4,30 +4,38 @@
============================================================ */
:root {
- /* Layered surfaces (darkest → lightest) */
- --bg-rail: #16171a;
- --bg-app: #1a1b1f;
- --bg-topbar: #1e1f24;
- --bg-content: #212228;
- --bg-panel: #282a31;
- --bg-input: #2f313a;
- --bg-hover: #363841;
- --bg-elev: #3a3c46;
+ /* Layered surfaces (darkest → lightest). A warm charcoal ramp (low-saturation
+ ~28° hue) rather than cool blue-grey — reads as "collection in warm light"
+ and harmonizes with the terracotta accent. Elevation model:
+ rail/app (chrome) → content (page) → panel (surface) → input (inset). */
+ --bg-rail: #191714;
+ --bg-app: #1d1a16;
+ --bg-topbar: #221e19;
+ --bg-content: #27221d;
+ --bg-panel: #322c25;
+ --bg-input: #3b342d;
+ --bg-hover: #443c34;
+ --bg-elev: #4c433a;
- --border: #34363e;
- --border-soft: rgba(255, 255, 255, 0.055);
+ --border: #3d372f;
+ --border-soft: rgba(255, 248, 240, 0.06);
+ /* Tactile 1px top inner-highlight for raised surfaces (see .panel). */
+ --panel-highlight: inset 0 1px 0 rgba(255, 246, 236, 0.045);
- --text: #dbdee4;
- --text-strong: #f5f6f8;
- --muted: #9a9ea8;
- --faint: #71757f;
+ --text: #e3ddd3;
+ --text-strong: #f8f4ec;
+ --muted: #a69d90;
+ --faint: #7d7568;
- /* Terracotta accent */
+ /* Terracotta accent (hue unchanged — the app's signature) */
--accent: #c8553d;
--accent-hover: #d86e56;
--accent-active: #a8412d;
--accent-subtle: rgba(200, 85, 61, 0.15);
--accent-glow: rgba(200, 85, 61, 0.35);
+ /* Quiet accent for decorative/label use, so full terracotta stays reserved
+ for primary actions + active state (accent hierarchy). */
+ --accent-quiet: #c69a86;
/* Complementary / status */
--info: #46a6bd; /* teal complement */
@@ -44,8 +52,39 @@
--mtg-g: #479b63;
--mtg-c: #9aa0ab;
- --radius: 8px;
- --radius-lg: 12px;
+ /* Spacing scale — every gap/margin/padding resolves to one of these. */
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-5: 20px;
+ --space-6: 24px;
+ --space-7: 32px;
+ --space-8: 48px;
+
+ /* Radius scale */
+ --radius-sm: 6px;
+ --radius: 9px;
+ --radius-lg: 14px;
+ --radius-card: 12px; /* card-art corners (matches Scryfall image framing) */
+ --radius-pill: 999px;
+
+ /* Type scale */
+ --fs-micro: 10px; /* dense data labels (under stat numbers, chart axes) */
+ --fs-caption: 12px;
+ --fs-body: 14px;
+ --fs-label: 11px; /* canonical uppercase micro-label, see .section-label */
+ --fs-title: 16px;
+ --fs-display: 21px;
+ --tracking-label: 0.06em;
+
+ /* Motion */
+ --dur-fast: 0.12s;
+ --dur: 0.2s;
+
+ /* Interactive control height — bumped on coarse pointers (see bottom). */
+ --control-h: 34px;
+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 6px 20px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 14px 44px rgba(0, 0, 0, 0.55);
@@ -210,6 +249,68 @@ input[type="checkbox"] {
cursor: pointer;
}
+/* ============================================================
+ Primitives: shared surface / label / page-header building blocks.
+ These replace treatments that were previously hand-copied (with drift)
+ across the views — edit here to change all of them at once.
+ ============================================================ */
+
+/* Canonical raised surface. Replaces the ~6 hand-copied panel treatments
+ (deck-meta, commander-hero, deck-stats, filters, legality-tile base,
+ preview-info). The top inner-highlight gives a subtle tactile edge. */
+.panel {
+ background: var(--bg-panel);
+ border: 1px solid var(--border-soft);
+ border-radius: var(--radius-lg);
+ padding: var(--space-4);
+ box-shadow: var(--panel-highlight);
+}
+/* Recessed variant for wells nested inside a panel. */
+.panel-inset {
+ background: var(--bg-app);
+ border: 1px solid var(--border-soft);
+ border-radius: var(--radius);
+ padding: var(--space-3);
+}
+
+/* Canonical uppercase section marker (panel/section headers). Uppercase +
+ tracking reads as structural; `--muted` (not faint) keeps it legible. */
+.section-label {
+ font-size: var(--fs-label);
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: var(--tracking-label);
+ color: var(--muted);
+}
+
+/* Standard page header: title on the left, control cluster on the right,
+ wrapping onto two lines on a narrow viewport. Replaces the ad-hoc
+ `
` each view used. */
+.page-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: var(--space-3);
+ margin-bottom: var(--space-4);
+}
+.page-header-title {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ margin: 0;
+ min-width: 0;
+}
+/* The right-hand control cluster (group-by + sort + view toggle + page
+ actions). Also usable standalone. */
+.page-header-actions,
+.toolbar {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: var(--space-4);
+}
+
/* ============================================================
App shell: nav rail + main
============================================================ */
@@ -338,11 +439,11 @@ input[type="checkbox"] {
margin-bottom: 10px;
}
.lib-stats-title {
- font-size: 12px;
+ font-size: var(--fs-label);
font-weight: 700;
text-transform: uppercase;
- letter-spacing: 0.07em;
- color: var(--faint);
+ letter-spacing: var(--tracking-label);
+ color: var(--muted);
}
.lib-stats-loading {
font-size: 12px;
@@ -381,7 +482,7 @@ input[type="checkbox"] {
text-shadow: 0 0 14px var(--accent-glow);
}
.stat-tile-label {
- font-size: 10px;
+ font-size: var(--fs-micro);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
@@ -582,7 +683,7 @@ input[type="checkbox"] {
.content {
flex: 1;
overflow: auto;
- padding: 22px 26px;
+ padding: var(--space-5) var(--space-6);
}
/* Empty / welcome state */
@@ -614,27 +715,28 @@ input[type="checkbox"] {
============================================================ */
h2 {
- margin: 2px 0 14px;
- font-size: 20px;
+ margin: var(--space-1) 0 var(--space-4);
+ font-size: var(--fs-display);
font-weight: 700;
color: var(--text-strong);
letter-spacing: -0.2px;
}
h3 {
- margin: 20px 0 12px;
- font-size: 16px;
+ margin: var(--space-5) 0 var(--space-3);
+ font-size: var(--fs-title);
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-strong);
border-bottom: 2px solid var(--border-soft);
- padding-bottom: 10px;
+ padding-bottom: var(--space-3);
}
h4 {
- margin: 14px 0 7px;
- font-size: 14px;
+ margin: var(--space-4) 0 var(--space-2);
+ font-size: var(--fs-body);
font-weight: 600;
- color: var(--accent-hover);
+ /* Quiet accent — full terracotta is reserved for primary actions. */
+ color: var(--accent-quiet);
}
/* "Group by" sub-headers (e.g. per card type/subtype/CMC/color within a
@@ -646,15 +748,16 @@ h4 {
position: sticky;
top: 0;
z-index: 5;
- margin: 14px 0 8px;
- padding: 8px 0;
+ margin: var(--space-4) 0 var(--space-2);
+ padding: var(--space-2) 0;
background: var(--bg-app);
- font-size: 15px;
+ font-size: var(--fs-body);
font-weight: 700;
text-transform: none;
letter-spacing: normal;
color: var(--text-strong);
- border-bottom: 2px solid var(--accent);
+ /* Quiet warm underline rather than full terracotta (accent hierarchy). */
+ border-bottom: 2px solid var(--accent-quiet);
}
.muted {
color: var(--muted);
@@ -673,10 +776,10 @@ h4 {
display: inline-flex;
align-items: center;
background: var(--accent-subtle);
- color: var(--accent-hover);
- border-radius: 999px;
- padding: 1px 9px;
- font-size: 12px;
+ color: var(--accent-quiet);
+ border-radius: var(--radius-pill);
+ padding: 1px var(--space-2);
+ font-size: var(--fs-caption);
font-weight: 700;
}
@@ -1232,12 +1335,12 @@ table {
}
th {
text-align: left;
- padding: 8px 12px;
- font-size: 12px;
+ padding: var(--space-2) var(--space-3);
+ font-size: var(--fs-label);
font-weight: 700;
text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--faint);
+ letter-spacing: var(--tracking-label);
+ color: var(--muted);
position: sticky;
top: 0;
background: var(--bg-content);
@@ -1339,27 +1442,53 @@ tbody tr:hover {
Filters panel
============================================================ */
+/* The workspace panel unifies the filter grid and the results toolbar into
+ one surface (Library + Card Search), so the display controls read as part
+ of the filtering apparatus instead of floating loosely above it. */
+.workspace {
+ margin-bottom: var(--space-4);
+}
+/* Just the responsive field grid; surface comes from the wrapping `.panel`. */
.filters {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
- gap: 14px;
- background: var(--bg-panel);
- border: 1px solid var(--border-soft);
- border-radius: var(--radius-lg);
- padding: 16px;
- margin-bottom: 16px;
+ gap: var(--space-4);
+}
+/* The results toolbar strip at the bottom of the workspace panel: selection +
+ count on the left, group/sort/view on the right, divided from the filters. */
+.workspace-bar {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: var(--space-3);
+ margin-top: var(--space-4);
+ padding-top: var(--space-4);
+ border-top: 1px solid var(--border-soft);
+}
+.workspace-bar-check {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-2);
+ cursor: pointer;
+ font-size: var(--fs-caption);
+ color: var(--muted);
+}
+.workspace-bar-count {
+ font-size: var(--fs-caption);
+ color: var(--faint);
+ font-variant-numeric: tabular-nums;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
+/* Form field labels: sentence-case and higher-contrast rather than tiny faint
+ all-caps — clearer to scan above each input than a structural section marker. */
.field > label {
- font-size: 12px;
+ font-size: var(--fs-caption);
font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.04em;
- color: var(--faint);
+ color: var(--muted);
}
.location-input {
padding: 4px 8px;
@@ -1631,12 +1760,12 @@ tbody tr:hover {
flex: 0 0 auto;
display: flex;
flex-direction: column;
- gap: 12px;
+ gap: var(--space-3);
background: var(--bg-panel);
border: 1px solid var(--border-soft);
- border-radius: 18px;
- padding: 18px;
- box-shadow: var(--shadow-lg);
+ border-radius: var(--radius-lg);
+ padding: var(--space-4);
+ box-shadow: var(--shadow-lg), var(--panel-highlight);
color: var(--text);
}
.preview-info-name {
@@ -1671,11 +1800,11 @@ tbody tr:hover {
border-top: 1px solid var(--border-soft);
}
.preview-info-label {
- font-size: 11px;
+ font-size: var(--fs-label);
font-weight: 700;
text-transform: uppercase;
- letter-spacing: 0.04em;
- color: var(--faint);
+ letter-spacing: var(--tracking-label);
+ color: var(--muted);
}
.preview-info-deck-chips {
display: flex;
@@ -1735,10 +1864,10 @@ tbody tr:hover {
.tile-img {
width: 100%;
display: block;
- border-radius: 12px;
+ border-radius: var(--radius-card);
box-shadow: var(--shadow-sm);
cursor: zoom-in;
- transition: transform 0.12s, box-shadow 0.12s;
+ transition: transform var(--dur-fast), box-shadow var(--dur-fast);
}
.tile:hover .tile-img {
transform: translateY(-3px);
@@ -1747,7 +1876,7 @@ tbody tr:hover {
.tile-noimg {
aspect-ratio: 488 / 680;
border: 1px solid var(--border);
- border-radius: 12px;
+ border-radius: var(--radius-card);
background: var(--bg-panel);
display: flex;
align-items: center;
@@ -1890,15 +2019,25 @@ tbody tr:hover {
Deck metadata (format + commander)
============================================================ */
+/* Two-pane deck screen: deck list + selected-deck detail. Stacks below the
+ tablet breakpoint (see the responsive section) so the list sits above the
+ detail on narrow/portrait viewports. */
+.deck-layout {
+ display: flex;
+ gap: var(--space-5);
+ align-items: flex-start;
+}
+.deck-sidebar {
+ width: 240px;
+ flex-shrink: 0;
+}
+
+/* Surface from `.panel`; layout only here. */
.deck-meta {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
- gap: 20px;
- background: var(--bg-panel);
- border: 1px solid var(--border-soft);
- border-radius: var(--radius-lg);
- padding: 14px 16px;
+ gap: var(--space-5);
}
/* Separates the deck-level info panels (commander, format, analysis,
legality) from the card-management controls + lists below. */
@@ -1957,15 +2096,12 @@ tbody tr:hover {
flex: 1;
}
-/* Prominent commander card display, above the fold on a Commander/Brawl deck. */
+/* Prominent commander card display, above the fold on a Commander/Brawl deck.
+ Surface from `.panel`; layout only here. */
.commander-hero {
display: flex;
align-items: center;
- gap: 16px;
- background: var(--bg-panel);
- border: 1px solid var(--border-soft);
- border-radius: var(--radius-lg);
- padding: 14px 16px;
+ gap: var(--space-4);
}
.commander-hero-img {
width: 84px;
@@ -1998,14 +2134,16 @@ tbody tr:hover {
gap: 5px;
}
.commander-hero-label {
- font-size: 10px;
+ font-size: var(--fs-micro);
font-weight: 700;
text-transform: uppercase;
- letter-spacing: 0.07em;
- color: var(--accent-hover);
+ letter-spacing: var(--tracking-label);
+ /* Quiet accent keeps a hint of brand on the hero without competing with
+ primary-action terracotta elsewhere. */
+ color: var(--accent-quiet);
}
.commander-hero-name {
- font-size: 19px;
+ font-size: var(--fs-display);
font-weight: 700;
color: var(--text-strong);
}
@@ -2014,20 +2152,9 @@ tbody tr:hover {
color: var(--muted);
}
-/* Per-deck analysis panel. */
-.deck-stats {
- background: var(--bg-panel);
- border: 1px solid var(--border-soft);
- border-radius: var(--radius-lg);
- padding: 14px 16px;
-}
+/* Per-deck analysis panel. Surface from `.panel`. */
.deck-stats-head {
- font-size: 11px;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.07em;
- color: var(--faint);
- margin-bottom: 10px;
+ margin-bottom: var(--space-3);
}
.deck-stats-body {
display: flex;
@@ -2048,11 +2175,84 @@ tbody tr:hover {
flex-direction: column;
}
.deck-stats-curve-label {
- font-size: 10px;
+ font-size: var(--fs-micro);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--faint);
- margin-bottom: 2px;
+ margin-bottom: var(--space-1);
+}
+
+/* ============================================================
+ Responsive groundwork
+ Desktop (the primary target) is unchanged above; these breakpoints keep
+ the layout usable as the window narrows and lay the foundation for a
+ future mobile port. Breakpoints: 1000 (tablet) and 720 (compact).
+ ============================================================ */
+
+/* Tablet: the deck two-pane stacks (list above detail). */
+@media (max-width: 1000px) {
+ .deck-layout {
+ flex-direction: column;
+ }
+ .deck-sidebar {
+ width: 100%;
+ }
+}
+
+/* Compact: tighten page gutters and collapse the nav rail to an icon strip
+ (same items — labels, wordmark, and the library summary hide; hover/title
+ still names each tab). No separate mobile navigation is introduced. */
+@media (max-width: 720px) {
+ .content {
+ padding: var(--space-4);
+ }
+ .nav-rail {
+ width: 60px;
+ padding: var(--space-3) var(--space-2) var(--space-2);
+ }
+ .brand {
+ justify-content: center;
+ padding: var(--space-1) 0 var(--space-4);
+ }
+ .brand-text {
+ display: none;
+ }
+ .nav-item {
+ justify-content: center;
+ padding: var(--space-2);
+ }
+ .nav-label {
+ display: none;
+ }
+ .nav-item.active::before {
+ left: -8px;
+ }
+ .nav-foot {
+ display: none;
+ }
+ .topbar {
+ padding: 0 var(--space-3);
+ }
+ /* The library path is the first thing to go when horizontal space is tight —
+ the library actions stay reachable. */
+ .path {
+ display: none;
+ }
+}
+
+/* Touch input (future mobile / touchscreen laptops): grow tap targets to a
+ comfortable size without inflating the compact mouse-driven desktop look. */
+@media (pointer: coarse) {
+ button,
+ input,
+ select,
+ .nav-item {
+ min-height: var(--control-h);
+ }
+ input[type="checkbox"] {
+ width: 18px;
+ height: 18px;
+ }
}
diff --git a/src/views/CardEntry.tsx b/src/views/CardEntry.tsx
index 8701c2e..8d268c8 100644
--- a/src/views/CardEntry.tsx
+++ b/src/views/CardEntry.tsx
@@ -78,9 +78,11 @@ export function CardEntry() {
return (
-
Add Cards
+
+
Add Cards
+
-
+
0 && (
<>
-
Just added
+
Just added
{recent.map((r, i) => (
-
diff --git a/src/views/CardSearch.tsx b/src/views/CardSearch.tsx
index a1b6410..7d2c3e6 100644
--- a/src/views/CardSearch.tsx
+++ b/src/views/CardSearch.tsx
@@ -267,11 +267,11 @@ export function CardSearch({
return (
-
-
+
+
Card Search {hasActiveFilter && {rows.length}}
-
+
-
- setSortDir(sortDir === "asc" ? "desc" : "asc")}
- />
-
+
@@ -357,6 +350,24 @@ export function CardSearch({
placeholder="e.g. landfall"
/>
+
+
+
+ {hasActiveFilter && (
+
+ {rows.length} result{rows.length === 1 ? "" : "s"}
+
+ )}
+
+
+ setSortDir(sortDir === "asc" ? "desc" : "asc")}
+ />
+
+
{!hasActiveFilter ? (
diff --git a/src/views/DeckView.tsx b/src/views/DeckView.tsx
index 6f239dd..4221354 100644
--- a/src/views/DeckView.tsx
+++ b/src/views/DeckView.tsx
@@ -159,7 +159,7 @@ function CommanderHero({ card, onClear }: { card: DeckEntry; onClear: () => void
const { handlers } = useHoverPreview();
const { open } = useClickPreview();
return (
-
+
{card.image_uri ? (
![]()
-
+
+
Decks
@@ -925,7 +925,7 @@ export function DeckView({ refreshSignal }: { refreshSignal?: number }) {
{/* Deck metadata: format + (for Commander/Brawl) the commander card. */}
-
+