Library → Import cards… now reads a ManaBox collection export, either the .csv or the .txt. The CSV is matched by Scryfall ID, falling back to name, set and collector number, and its Foil column picks the copy group's finish. Condition, language and prices are ignored; proxies count as owned. A decklist line that names a printing now resolves to that printing's card when several cards share the name (a card and its token, or an art-series entry), and a bare name prefers the real card. Imports run in one transaction (nestable via SAVEPOINT), which took the 942-row sample from 22s to 0.2s, and off the main thread, with a progress dialog for slow ones. An explainer of the supported formats now comes before the file picker.
3577 lines
86 KiB
CSS
3577 lines
86 KiB
CSS
/* ============================================================
|
||
mtgc — design system
|
||
Terracotta accent (#C8553D) on a layered, Discord-inspired dark UI.
|
||
============================================================ */
|
||
|
||
:root {
|
||
/* 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: #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: #e3ddd3;
|
||
--text-strong: #f8f4ec;
|
||
--muted: #a69d90;
|
||
--faint: #7d7568;
|
||
|
||
/* 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 */
|
||
--gold: #e0b24a;
|
||
--success: #4bb073;
|
||
--warn: #e0a34a;
|
||
--danger: #d8503f;
|
||
|
||
/* WUBRG mana colors, shared by pips + the library color-distribution bar */
|
||
--mtg-w: #f4efdd;
|
||
--mtg-u: #3a8fd0;
|
||
--mtg-b: #38343d;
|
||
--mtg-r: var(--accent);
|
||
--mtg-g: #479b63;
|
||
--mtg-c: #9aa0ab;
|
||
|
||
/* 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;
|
||
/* Height of .bottom-nav's own content — excludes the safe-area inset added
|
||
on top of it, so consumers computing clearance must add that separately. */
|
||
--bottom-nav-h: 58px;
|
||
|
||
--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);
|
||
|
||
color-scheme: dark;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html,
|
||
body,
|
||
#root {
|
||
height: 100%;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
font-family: "Inter", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif;
|
||
background: var(--bg-app);
|
||
color: var(--text);
|
||
font-size: 15px;
|
||
line-height: 1.5;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
::selection {
|
||
background: var(--accent-subtle);
|
||
}
|
||
|
||
/* Thin, unobtrusive scrollbars */
|
||
*::-webkit-scrollbar {
|
||
width: 11px;
|
||
height: 11px;
|
||
}
|
||
*::-webkit-scrollbar-thumb {
|
||
background: rgba(0, 0, 0, 0.35);
|
||
border-radius: 8px;
|
||
border: 3px solid transparent;
|
||
background-clip: padding-box;
|
||
}
|
||
*::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(0, 0, 0, 0.5);
|
||
background-clip: padding-box;
|
||
}
|
||
*::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
/* -------- Controls -------- */
|
||
|
||
button {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
background: var(--bg-input);
|
||
color: var(--text);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
padding: 7px 13px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
font-family: inherit;
|
||
transition: background 0.12s, border-color 0.12s, transform 0.05s, filter 0.12s;
|
||
}
|
||
button svg {
|
||
flex-shrink: 0;
|
||
}
|
||
button:hover {
|
||
background: var(--bg-hover);
|
||
border-color: var(--bg-elev);
|
||
}
|
||
button:active {
|
||
transform: translateY(1px);
|
||
}
|
||
button:disabled {
|
||
opacity: 0.5;
|
||
cursor: default;
|
||
transform: none;
|
||
}
|
||
button.primary {
|
||
background: linear-gradient(180deg, var(--accent-hover), var(--accent));
|
||
border: none;
|
||
color: #fff;
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
button.primary:hover {
|
||
filter: brightness(1.07);
|
||
background: linear-gradient(180deg, var(--accent-hover), var(--accent));
|
||
}
|
||
button.primary:active {
|
||
background: var(--accent-active);
|
||
}
|
||
button.danger {
|
||
background: var(--danger);
|
||
border: none;
|
||
color: #fff;
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
button.danger:hover {
|
||
filter: brightness(1.1);
|
||
}
|
||
button.danger:active {
|
||
filter: brightness(0.92);
|
||
}
|
||
button.big {
|
||
padding: 11px 22px;
|
||
font-size: 16px;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
input,
|
||
select {
|
||
background: var(--bg-input);
|
||
color: var(--text);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
padding: 8px 11px;
|
||
font-size: 14px;
|
||
font-family: inherit;
|
||
transition: border-color 0.12s, box-shadow 0.12s;
|
||
}
|
||
input::placeholder {
|
||
color: var(--faint);
|
||
}
|
||
input:focus,
|
||
select:focus {
|
||
outline: none;
|
||
border-color: var(--accent);
|
||
box-shadow: 0 0 0 3px var(--accent-subtle);
|
||
}
|
||
|
||
/* Native <select> chrome (height, arrow, popup panel) ignores color/border
|
||
overrides unless its OS-drawn appearance is dropped first. */
|
||
select {
|
||
appearance: none;
|
||
-webkit-appearance: none;
|
||
-moz-appearance: none;
|
||
padding-right: 30px;
|
||
cursor: pointer;
|
||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239a9ea8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
|
||
background-repeat: no-repeat;
|
||
background-position: right 9px center;
|
||
background-size: 14px;
|
||
}
|
||
select:hover {
|
||
border-color: var(--bg-elev);
|
||
}
|
||
/* The dropdown's own option list stays OS-native (no cross-browser hook to
|
||
restyle it), but at least match its swatch to the app's dark theme. */
|
||
select option {
|
||
background: var(--bg-panel);
|
||
color: var(--text);
|
||
}
|
||
|
||
input[type="checkbox"] {
|
||
width: 15px;
|
||
height: 15px;
|
||
accent-color: var(--accent);
|
||
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
|
||
`<div class="row" style="justify-content:space-between">` 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
|
||
============================================================ */
|
||
|
||
.app {
|
||
display: flex;
|
||
/* `dvh` tracks the *dynamic* viewport, so an on-screen keyboard (or a mobile
|
||
browser's collapsing chrome) shrinks the shell instead of pushing content
|
||
out of an over-tall, unscrollable `100vh` box. Falls back for older
|
||
engines that don't know dvh. */
|
||
height: 100vh;
|
||
height: 100dvh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.nav-rail {
|
||
width: 220px;
|
||
flex-shrink: 0;
|
||
background: var(--bg-rail);
|
||
border-right: 1px solid var(--border-soft);
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 14px 10px 10px;
|
||
}
|
||
.brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 11px;
|
||
padding: 4px 8px 16px;
|
||
}
|
||
.brand-mark {
|
||
width: 34px;
|
||
height: 34px;
|
||
border-radius: 10px;
|
||
background: linear-gradient(150deg, var(--bg-elev), var(--bg-rail));
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
.brand-text {
|
||
font-weight: 700;
|
||
font-size: 16px;
|
||
color: var(--text-strong);
|
||
letter-spacing: 0.2px;
|
||
}
|
||
.nav-items {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 3px;
|
||
}
|
||
.nav-item {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
width: 100%;
|
||
text-align: left;
|
||
padding: 9px 12px;
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
background: transparent;
|
||
color: var(--muted);
|
||
font-size: 15px;
|
||
font-weight: 550;
|
||
}
|
||
.nav-item:hover {
|
||
background: rgba(255, 255, 255, 0.04);
|
||
color: var(--text);
|
||
}
|
||
.nav-item.active {
|
||
background: var(--accent-subtle);
|
||
color: var(--text-strong);
|
||
}
|
||
.nav-item.active::before {
|
||
content: "";
|
||
position: absolute;
|
||
left: -10px;
|
||
top: 9px;
|
||
bottom: 9px;
|
||
width: 4px;
|
||
border-radius: 0 4px 4px 0;
|
||
background: var(--accent);
|
||
}
|
||
.nav-icon {
|
||
display: inline-flex;
|
||
color: inherit;
|
||
}
|
||
|
||
/* Android-style bottom tab bar — replaces .nav-rail on narrow viewports (see
|
||
the ≤720px block below), rather than the rail collapsing to an icon strip.
|
||
Hidden by default; enabled only in that breakpoint. Fixed to the viewport
|
||
(not `.app`) since `.app` uses `overflow: hidden` and z-indexes elsewhere
|
||
(dialogs 400+, the click-preview modal 450) are the reason this sits at
|
||
200 — above ordinary content, below anything that should cover the whole
|
||
screen. */
|
||
.bottom-nav {
|
||
display: none;
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 200;
|
||
height: calc(var(--bottom-nav-h) + env(safe-area-inset-bottom));
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
background: var(--bg-rail);
|
||
border-top: 1px solid var(--border-soft);
|
||
}
|
||
.bottom-nav-item {
|
||
position: relative;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 3px;
|
||
height: 100%;
|
||
padding: 0 2px;
|
||
border: none;
|
||
border-radius: 0;
|
||
background: transparent;
|
||
color: var(--muted);
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
}
|
||
.bottom-nav-item.active {
|
||
color: var(--accent);
|
||
}
|
||
.bottom-nav-item.active::before {
|
||
content: "";
|
||
position: absolute;
|
||
top: 0;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 28px;
|
||
height: 3px;
|
||
border-radius: 0 0 3px 3px;
|
||
background: var(--accent);
|
||
}
|
||
.nav-foot {
|
||
margin-top: auto;
|
||
padding: 14px 10px 10px;
|
||
border-top: 1px solid var(--border-soft);
|
||
position: relative;
|
||
}
|
||
.nav-foot::before {
|
||
/* A faint accent hairline atop the divider, echoing the active-tab bar. */
|
||
content: "";
|
||
position: absolute;
|
||
top: -1px;
|
||
left: 10px;
|
||
right: 10px;
|
||
height: 1px;
|
||
background: linear-gradient(90deg, transparent, var(--accent-glow), transparent);
|
||
}
|
||
.nav-foot-simple {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
}
|
||
.status-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
.status-dot.on {
|
||
background: var(--success);
|
||
box-shadow: 0 0 6px var(--success);
|
||
}
|
||
.status-dot.off {
|
||
background: var(--faint);
|
||
}
|
||
.status-dot.warn {
|
||
background: var(--warn);
|
||
}
|
||
|
||
/* Sidebar "your library" summary panel. */
|
||
.lib-stats-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 10px;
|
||
}
|
||
.lib-stats-title {
|
||
font-size: var(--fs-label);
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: var(--tracking-label);
|
||
color: var(--muted);
|
||
}
|
||
.lib-stats-loading {
|
||
font-size: 12px;
|
||
color: var(--faint);
|
||
padding-bottom: 10px;
|
||
}
|
||
/* Generic numeric-stat tile row + segmented color bar — used by both the
|
||
sidebar "Your library" panel and the per-deck analysis panel. */
|
||
.stat-tiles-row {
|
||
display: flex;
|
||
gap: 6px;
|
||
margin-bottom: 10px;
|
||
}
|
||
.stat-tile {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 2px;
|
||
padding: 8px 2px;
|
||
background: var(--bg-panel);
|
||
border: 1px solid var(--border-soft);
|
||
border-radius: var(--radius);
|
||
transition: border-color 0.15s, transform 0.15s;
|
||
}
|
||
.stat-tile:hover {
|
||
border-color: var(--accent-glow);
|
||
transform: translateY(-1px);
|
||
}
|
||
.stat-tile-value {
|
||
font-size: 17px;
|
||
font-weight: 800;
|
||
color: var(--text-strong);
|
||
font-variant-numeric: tabular-nums;
|
||
text-shadow: 0 0 14px var(--accent-glow);
|
||
}
|
||
.stat-tile-label {
|
||
font-size: var(--fs-micro);
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
color: var(--faint);
|
||
}
|
||
.color-bar {
|
||
display: flex;
|
||
height: 8px;
|
||
border-radius: 999px;
|
||
overflow: hidden;
|
||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
|
||
margin-bottom: 9px;
|
||
}
|
||
.color-bar-seg {
|
||
min-width: 3px;
|
||
transition: flex-grow 0.4s ease;
|
||
}
|
||
.color-bar-seg.W { background: var(--mtg-w); }
|
||
.color-bar-seg.U { background: var(--mtg-u); }
|
||
.color-bar-seg.B { background: var(--mtg-b); }
|
||
.color-bar-seg.R { background: var(--mtg-r); }
|
||
.color-bar-seg.G { background: var(--mtg-g); }
|
||
.color-bar-seg.C { background: var(--mtg-c); }
|
||
|
||
/* Mana-value curve histogram (per-deck analysis panel). */
|
||
.mana-curve {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 6px;
|
||
flex: 1;
|
||
min-height: 84px;
|
||
margin-top: 4px;
|
||
}
|
||
.mana-curve-col {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
height: 100%;
|
||
gap: 3px;
|
||
}
|
||
.mana-curve-count {
|
||
font-size: 10px;
|
||
color: var(--faint);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.mana-curve-bar {
|
||
width: 100%;
|
||
min-height: 2px;
|
||
border-radius: 3px 3px 0 0;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
transition: height 0.25s ease;
|
||
}
|
||
.mana-curve-seg {
|
||
width: 100%;
|
||
min-height: 1px;
|
||
}
|
||
.mana-curve-seg.W { background: var(--mtg-w); }
|
||
.mana-curve-seg.U { background: var(--mtg-u); }
|
||
.mana-curve-seg.B { background: var(--mtg-b); }
|
||
.mana-curve-seg.R { background: var(--mtg-r); }
|
||
.mana-curve-seg.G { background: var(--mtg-g); }
|
||
.mana-curve-seg.C { background: var(--mtg-c); }
|
||
.mana-curve-label {
|
||
font-size: 10px;
|
||
color: var(--faint);
|
||
}
|
||
.lib-stats-sub {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
font-size: 11px;
|
||
color: var(--faint);
|
||
}
|
||
|
||
.main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: var(--bg-content);
|
||
}
|
||
.topbar {
|
||
/* min-height (not height): on Android the window draws edge-to-edge behind
|
||
the status bar, so the safe-area inset has to ADD to the bar's height
|
||
rather than eat into it — a fixed height would squash the buttons under
|
||
the status bar instead of below it. Inert (0) anywhere without a system
|
||
bar to avoid, e.g. desktop. */
|
||
min-height: 52px;
|
||
padding-top: env(safe-area-inset-top);
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding-left: 18px;
|
||
padding-right: 18px;
|
||
background: var(--bg-topbar);
|
||
border-bottom: 1px solid var(--border-soft);
|
||
}
|
||
.path {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
font-size: 13px;
|
||
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||
color: var(--faint);
|
||
background: rgba(0, 0, 0, 0.25);
|
||
padding: 4px 10px;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.spacer {
|
||
flex: 1;
|
||
}
|
||
|
||
/* Ingest (card-data download/index) progress: one multi-segment bar spanning
|
||
all stages, below the topbar. */
|
||
.ingest-bar {
|
||
padding: 8px 18px 10px;
|
||
background: var(--bg-elev);
|
||
border-bottom: 1px solid var(--border-soft);
|
||
font-size: 13px;
|
||
}
|
||
/* Card data from an older version: same strip as the ingest bar, tinted as
|
||
a warning. Wraps its buttons under the message on narrow screens. */
|
||
.outdated-bar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px 16px;
|
||
padding: 8px 18px;
|
||
background: rgba(224, 163, 74, 0.12);
|
||
border-bottom: 1px solid rgba(224, 163, 74, 0.35);
|
||
font-size: 13px;
|
||
}
|
||
.outdated-bar-message {
|
||
flex: 1 1 260px;
|
||
color: var(--text);
|
||
}
|
||
.outdated-bar-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
.ingest-bar-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 6px;
|
||
}
|
||
.ingest-bar-message {
|
||
color: var(--muted);
|
||
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.ingest-bar-cancel {
|
||
flex-shrink: 0;
|
||
padding: 3px 10px;
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
.ingest-bar-cancel:hover {
|
||
color: var(--danger);
|
||
border-color: var(--danger);
|
||
}
|
||
.ingest-segments {
|
||
display: flex;
|
||
gap: 4px;
|
||
}
|
||
.ingest-segment {
|
||
min-width: 0;
|
||
}
|
||
.ingest-segment-track {
|
||
height: 6px;
|
||
border-radius: 999px;
|
||
background: rgba(0, 0, 0, 0.3);
|
||
overflow: hidden;
|
||
}
|
||
.ingest-segment-fill {
|
||
height: 100%;
|
||
border-radius: 999px;
|
||
background: var(--border);
|
||
transition: width 0.2s ease;
|
||
}
|
||
.progress-dialog-bar {
|
||
margin: var(--space-3) 0;
|
||
}
|
||
.ingest-segment.active .ingest-segment-fill {
|
||
background: linear-gradient(90deg, var(--accent-hover), var(--accent));
|
||
}
|
||
.ingest-segment-label {
|
||
display: block;
|
||
margin-top: 3px;
|
||
font-size: 11px;
|
||
color: var(--faint);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.ingest-segment.active .ingest-segment-label {
|
||
color: var(--accent-hover);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.ingest-bar-done .ingest-segment-fill {
|
||
background: var(--success);
|
||
}
|
||
.ingest-bar-done .ingest-bar-message {
|
||
color: var(--success);
|
||
}
|
||
.ingest-bar-error .ingest-segment-fill {
|
||
background: var(--danger);
|
||
}
|
||
.ingest-bar-error .ingest-bar-message {
|
||
color: var(--danger);
|
||
}
|
||
.ingest-bar-cancelled .ingest-segment-fill {
|
||
background: var(--warn);
|
||
}
|
||
.ingest-bar-cancelled .ingest-bar-message {
|
||
color: var(--warn);
|
||
}
|
||
.content {
|
||
flex: 1;
|
||
overflow: auto;
|
||
padding: var(--space-5) var(--space-6);
|
||
}
|
||
|
||
/* Empty / welcome state */
|
||
.empty-state {
|
||
max-width: 480px;
|
||
margin: 9vh auto 0;
|
||
text-align: center;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 14px;
|
||
}
|
||
.empty-mark {
|
||
width: 88px;
|
||
height: 88px;
|
||
border-radius: 22px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: linear-gradient(150deg, var(--bg-elev), var(--bg-rail));
|
||
box-shadow: 0 10px 30px var(--accent-glow);
|
||
}
|
||
.empty-state h2 {
|
||
margin: 4px 0 0;
|
||
}
|
||
|
||
/* Mobile's Info tab — card-database management, which lives in the topbar
|
||
instead on desktop (see INFO_TAB in App.tsx). */
|
||
.info-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-3);
|
||
max-width: 480px;
|
||
}
|
||
/* Top-bar sync state; opens the Info tab. Quiet when all is well, so it
|
||
only draws the eye when something needs attention. */
|
||
.sync-indicator {
|
||
flex-shrink: 0;
|
||
font-size: var(--fs-caption);
|
||
padding: 5px 10px;
|
||
white-space: nowrap;
|
||
}
|
||
.sync-indicator-warn {
|
||
border-color: var(--warn);
|
||
color: var(--warn);
|
||
}
|
||
/* First sync with an account that already has a library: a decision only the
|
||
user can make, so it's set apart from the surrounding status text. */
|
||
.sync-choice {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-2);
|
||
padding: var(--space-3);
|
||
border: 1px solid var(--warn);
|
||
border-radius: var(--radius);
|
||
}
|
||
.sync-devices {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-2);
|
||
}
|
||
.sync-devices li {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
flex-wrap: wrap;
|
||
}
|
||
.sync-device-name {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
font-weight: 600;
|
||
color: var(--text-strong);
|
||
}
|
||
.sync-devices .link-button {
|
||
margin-left: auto;
|
||
background: none;
|
||
border: none;
|
||
padding: 2px 4px;
|
||
color: var(--danger);
|
||
}
|
||
/* The signed-in account. */
|
||
.sync-folder {
|
||
font-size: var(--fs-body);
|
||
font-weight: 600;
|
||
color: var(--text-strong);
|
||
overflow-wrap: anywhere;
|
||
}
|
||
/* Outcome of the synced-folder self-test. Not a `.hint`: whether sync works is
|
||
the point of the section, so it reads at body size rather than as
|
||
de-emphasized supporting text. */
|
||
.sync-ok,
|
||
.sync-bad {
|
||
font-size: var(--fs-body);
|
||
line-height: 1.45;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.sync-ok {
|
||
color: var(--success);
|
||
}
|
||
.sync-bad {
|
||
color: var(--danger);
|
||
}
|
||
|
||
/* ============================================================
|
||
Typography helpers
|
||
============================================================ */
|
||
|
||
h2 {
|
||
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: 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: var(--space-3);
|
||
}
|
||
h4 {
|
||
margin: var(--space-4) 0 var(--space-2);
|
||
font-size: var(--fs-body);
|
||
font-weight: 600;
|
||
/* 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
|
||
* section) — sticky within .content's scroll area so the current group
|
||
* stays visible while scrolling through a long list, instead of scrolling
|
||
* out of view along with its cards. Applied via class (not the bare h3/h4
|
||
* tag) since h3 is also used for .dialog-title, which must not be sticky. */
|
||
.group-header {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 5;
|
||
margin: var(--space-4) 0 var(--space-2);
|
||
padding: var(--space-2) 0;
|
||
background: var(--bg-app);
|
||
font-size: var(--fs-body);
|
||
font-weight: 700;
|
||
text-transform: none;
|
||
letter-spacing: normal;
|
||
color: var(--text-strong);
|
||
/* Quiet warm underline rather than full terracotta (accent hierarchy). */
|
||
border-bottom: 2px solid var(--accent-quiet);
|
||
}
|
||
.muted {
|
||
color: var(--muted);
|
||
}
|
||
.hint {
|
||
color: var(--muted);
|
||
font-size: 14px;
|
||
line-height: 1.65;
|
||
}
|
||
.row {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
.badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
background: var(--accent-subtle);
|
||
color: var(--accent-quiet);
|
||
border-radius: var(--radius-pill);
|
||
padding: 1px var(--space-2);
|
||
font-size: var(--fs-caption);
|
||
font-weight: 700;
|
||
}
|
||
|
||
/* Deck format-legality panel: a prominent, colorful tile, matching
|
||
.commander-hero / .deck-stats in weight, above the fold on every deck. */
|
||
.legality-tile {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
border: 1px solid transparent;
|
||
border-radius: var(--radius-lg);
|
||
padding: 14px 16px;
|
||
}
|
||
.legality-tile.legal {
|
||
background: rgba(75, 176, 115, 0.16);
|
||
border-color: rgba(75, 176, 115, 0.4);
|
||
}
|
||
.legality-tile.illegal {
|
||
background: rgba(216, 80, 63, 0.16);
|
||
border-color: rgba(216, 80, 63, 0.4);
|
||
}
|
||
.legality-tile.unchecked {
|
||
background: var(--bg-panel);
|
||
border-color: var(--border-soft);
|
||
}
|
||
.legality-tile-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
}
|
||
.legality-tile-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 999px;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
}
|
||
.legality-tile.legal .legality-tile-icon {
|
||
background: var(--success);
|
||
color: #fff;
|
||
}
|
||
.legality-tile.illegal .legality-tile-icon {
|
||
background: var(--danger);
|
||
color: #fff;
|
||
}
|
||
.legality-tile.unchecked .legality-tile-icon {
|
||
background: var(--muted);
|
||
color: var(--bg-panel);
|
||
}
|
||
.legality-tile-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
.legality-tile-status {
|
||
font-weight: 700;
|
||
font-size: 14px;
|
||
}
|
||
.legality-tile.legal .legality-tile-status {
|
||
color: var(--success);
|
||
}
|
||
.legality-tile.illegal .legality-tile-status {
|
||
color: var(--danger);
|
||
}
|
||
.legality-tile-detail {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
.legality-tile-reasons {
|
||
margin: 0 0 0 54px;
|
||
padding-left: 18px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
font-size: 13px;
|
||
color: var(--text);
|
||
}
|
||
|
||
/* ============================================================
|
||
Quantity stepper: [− N +] for owned-copy counts. Used on Add Cards
|
||
and both Library views (list + tile) for a consistent look.
|
||
============================================================ */
|
||
.qty-stepper {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
height: 26px;
|
||
border-radius: 999px;
|
||
background: var(--bg-input);
|
||
border: 1px solid var(--border);
|
||
overflow: hidden;
|
||
transition: opacity 0.25s ease, background 0.15s ease, border-color 0.15s ease;
|
||
}
|
||
.qty-stepper-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 26px;
|
||
height: 100%;
|
||
padding: 0;
|
||
background: none;
|
||
border: none;
|
||
border-radius: 0;
|
||
color: var(--muted);
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
transition: background 0.12s, color 0.12s;
|
||
}
|
||
.qty-stepper-btn:hover:not(:disabled) {
|
||
background: var(--accent-subtle);
|
||
color: var(--accent-hover);
|
||
}
|
||
.qty-stepper-btn:active:not(:disabled) {
|
||
background: var(--accent);
|
||
color: #fff;
|
||
transform: none;
|
||
}
|
||
.qty-stepper-btn:disabled {
|
||
opacity: 0.3;
|
||
cursor: default;
|
||
}
|
||
.qty-stepper-value {
|
||
min-width: 22px;
|
||
text-align: center;
|
||
padding: 0 2px;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: var(--text-strong);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.qty-stepper.warn {
|
||
border-color: var(--warn);
|
||
}
|
||
.qty-stepper.warn .qty-stepper-value {
|
||
color: var(--warn);
|
||
}
|
||
.qty-stepper.pending {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
/* Overlay variant: dark translucent pill for use atop card art. */
|
||
.qty-stepper-overlay {
|
||
height: 24px;
|
||
background: rgba(0, 0, 0, 0.78);
|
||
border: none;
|
||
backdrop-filter: blur(2px);
|
||
}
|
||
.qty-stepper-overlay .qty-stepper-btn {
|
||
width: 22px;
|
||
color: rgba(255, 255, 255, 0.85);
|
||
}
|
||
.qty-stepper-overlay .qty-stepper-btn:hover:not(:disabled) {
|
||
background: rgba(255, 255, 255, 0.18);
|
||
color: #fff;
|
||
}
|
||
.qty-stepper-overlay .qty-stepper-btn:active:not(:disabled) {
|
||
background: var(--accent);
|
||
color: #fff;
|
||
}
|
||
.qty-stepper-overlay .qty-stepper-value {
|
||
color: #fff;
|
||
}
|
||
.qty-stepper-overlay.warn {
|
||
background: var(--warn);
|
||
}
|
||
.qty-stepper-overlay.warn .qty-stepper-value,
|
||
.qty-stepper-overlay.warn .qty-stepper-btn {
|
||
color: #241700;
|
||
}
|
||
.qty-stepper-overlay.warn .qty-stepper-btn:hover:not(:disabled) {
|
||
background: rgba(0, 0, 0, 0.12);
|
||
}
|
||
|
||
/* ============================================================
|
||
Icon buttons: rename, duplicate, delete, dismiss, close — one
|
||
consistent look used everywhere a bare icon-only action appears.
|
||
============================================================ */
|
||
.icon-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
width: 28px;
|
||
height: 28px;
|
||
padding: 0;
|
||
border-radius: 999px;
|
||
background: var(--bg-input);
|
||
border: 1px solid var(--border);
|
||
color: var(--muted);
|
||
}
|
||
.icon-btn:hover:not(:disabled) {
|
||
background: var(--bg-hover);
|
||
color: var(--text);
|
||
border-color: var(--bg-elev);
|
||
}
|
||
.icon-btn:active:not(:disabled) {
|
||
transform: translateY(1px);
|
||
}
|
||
.icon-btn:disabled {
|
||
opacity: 0.35;
|
||
}
|
||
.icon-btn.icon-btn-danger:hover:not(:disabled) {
|
||
background: rgba(216, 80, 63, 0.15);
|
||
color: var(--danger);
|
||
border-color: var(--danger);
|
||
}
|
||
|
||
/* Overlay variant: dark translucent, for sitting on top of card art. */
|
||
.icon-btn-overlay {
|
||
width: 26px;
|
||
height: 26px;
|
||
background: rgba(0, 0, 0, 0.78);
|
||
border: none;
|
||
color: rgba(255, 255, 255, 0.85);
|
||
backdrop-filter: blur(2px);
|
||
}
|
||
.icon-btn-overlay:hover:not(:disabled) {
|
||
background: rgba(255, 255, 255, 0.18);
|
||
color: #fff;
|
||
}
|
||
.icon-btn-overlay.icon-btn-danger:hover:not(:disabled) {
|
||
background: var(--danger);
|
||
color: #fff;
|
||
}
|
||
|
||
/* Row-level action icons (e.g. rename/duplicate/delete a deck): hidden until
|
||
* the row is hovered/focused, so the collapsed list stays uncluttered. */
|
||
.row-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
flex-shrink: 0;
|
||
opacity: 0;
|
||
transition: opacity 0.12s;
|
||
}
|
||
li:hover .row-actions,
|
||
li:focus-within .row-actions {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* ============================================================
|
||
Dialogs: confirm (delete) / prompt (rename, duplicate-as)
|
||
============================================================ */
|
||
.dialog-backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 400;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(10, 10, 12, 0.72);
|
||
backdrop-filter: blur(2px);
|
||
animation: preview-fade-in 0.12s ease;
|
||
}
|
||
.dialog {
|
||
width: min(380px, 90vw);
|
||
background: var(--bg-panel);
|
||
border: 1px solid var(--border-soft);
|
||
border-radius: var(--radius-lg);
|
||
padding: 20px;
|
||
box-shadow: var(--shadow-lg);
|
||
}
|
||
.dialog-wide {
|
||
width: min(900px, 92vw);
|
||
max-height: 86vh;
|
||
overflow-y: auto;
|
||
}
|
||
.dialog-title-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 10px;
|
||
}
|
||
.dialog-title {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
text-transform: none;
|
||
letter-spacing: normal;
|
||
color: var(--text-strong);
|
||
border-bottom: none;
|
||
padding-bottom: 0;
|
||
}
|
||
.dialog-message {
|
||
margin: 0 0 18px;
|
||
color: var(--muted);
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
.dialog-list {
|
||
margin: 0 0 18px;
|
||
padding-left: 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
max-height: 320px;
|
||
overflow-y: auto;
|
||
color: var(--muted);
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
.dialog-list code {
|
||
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||
font-size: 13px;
|
||
color: var(--text-strong);
|
||
white-space: nowrap;
|
||
}
|
||
.dialog-field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
margin-bottom: 14px;
|
||
}
|
||
.dialog-field input {
|
||
width: 100%;
|
||
}
|
||
.dialog-error {
|
||
margin: -6px 0 14px;
|
||
color: var(--danger);
|
||
font-size: 13px;
|
||
}
|
||
.dialog-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* ============================================================
|
||
Lists (search results, deck list)
|
||
============================================================ */
|
||
|
||
.result-list {
|
||
list-style: none;
|
||
margin: 8px 0 0;
|
||
padding: 0;
|
||
border: 1px solid var(--border-soft);
|
||
border-radius: var(--radius-lg);
|
||
overflow: hidden;
|
||
background: var(--bg-panel);
|
||
}
|
||
.result-list li {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 9px 12px;
|
||
border-bottom: 1px solid var(--border-soft);
|
||
cursor: pointer;
|
||
transition: background 0.1s;
|
||
}
|
||
.result-list li:last-child {
|
||
border-bottom: none;
|
||
}
|
||
.result-list li:hover {
|
||
background: var(--bg-hover);
|
||
}
|
||
.card-name {
|
||
font-weight: 600;
|
||
color: var(--text-strong);
|
||
}
|
||
|
||
/* Deck list: name+format on one row, actions on their own row below, so
|
||
* long names don't push the action icons off the edge of the sidebar. */
|
||
.result-list.deck-list li {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 4px;
|
||
}
|
||
.deck-list-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
.deck-list-row .card-name {
|
||
overflow-wrap: break-word;
|
||
}
|
||
.deck-list-actions-row {
|
||
justify-content: space-between;
|
||
}
|
||
|
||
/* Suggestions list: a thumbnail + a compact name/badges header + wrapped
|
||
* synergy reason chips per row, with stacked actions on the right. Denser
|
||
* than the default .result-list row (smaller padding/gaps, chips instead of
|
||
* a bulleted list) since a card can carry several reasons at once. */
|
||
.result-list.suggestion-list li {
|
||
cursor: default;
|
||
align-items: stretch;
|
||
padding: 8px 12px;
|
||
}
|
||
.suggestion-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
width: 100%;
|
||
}
|
||
.suggestion-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
.suggestion-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.suggestion-score {
|
||
font-weight: 700;
|
||
color: var(--accent-hover);
|
||
font-size: 13px;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.suggestion-reasons {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
}
|
||
.reason-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
border-radius: 999px;
|
||
padding: 1px 8px 1px 2px;
|
||
font-size: 11px;
|
||
line-height: 1.6;
|
||
background: var(--bg-elev);
|
||
color: var(--muted);
|
||
}
|
||
.reason-chip-category {
|
||
border-radius: 999px;
|
||
padding: 1px 7px;
|
||
font-weight: 700;
|
||
font-size: 10px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.03em;
|
||
color: #fff;
|
||
}
|
||
.reason-chip-tribal .reason-chip-category {
|
||
background: var(--accent);
|
||
}
|
||
.reason-chip-mechanic .reason-chip-category {
|
||
background: var(--mtg-u);
|
||
}
|
||
.reason-chip-archetype .reason-chip-category {
|
||
background: var(--warn);
|
||
}
|
||
.reason-chip-mana-base .reason-chip-category {
|
||
background: var(--success);
|
||
}
|
||
.suggestion-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
flex-shrink: 0;
|
||
}
|
||
.suggestion-actions button {
|
||
font-size: 12px;
|
||
padding: 5px 10px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Toggleable "filter by why" chips above the suggestion list — same color
|
||
* coding as .reason-chip-category, but as clickable, stateful buttons. */
|
||
.suggestion-filters {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
margin-bottom: 10px;
|
||
}
|
||
.filter-chip {
|
||
border-radius: 999px;
|
||
padding: 3px 12px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
background: var(--bg-elev);
|
||
color: var(--muted);
|
||
border: 1px solid var(--border-soft);
|
||
}
|
||
.filter-chip:hover {
|
||
background: var(--bg-hover);
|
||
}
|
||
.filter-chip.active {
|
||
/* Plain on/off chips ("Unusual picks"); the category chips below swap in
|
||
their own colour. */
|
||
background: var(--accent);
|
||
color: #fff;
|
||
border-color: transparent;
|
||
}
|
||
.filter-chip-tribal.active {
|
||
background: var(--accent);
|
||
}
|
||
.filter-chip-mechanic.active {
|
||
background: var(--mtg-u);
|
||
}
|
||
.filter-chip-archetype.active {
|
||
background: var(--warn);
|
||
}
|
||
.filter-chip-mana-base.active {
|
||
background: var(--success);
|
||
}
|
||
.filter-chip-clear {
|
||
background: none;
|
||
border: none;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
text-decoration: underline;
|
||
padding: 3px 4px;
|
||
}
|
||
|
||
.card-type {
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* ============================================================
|
||
Color pips (WUBRG)
|
||
============================================================ */
|
||
|
||
.pips {
|
||
display: inline-flex;
|
||
gap: 3px;
|
||
}
|
||
.pip {
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 50%;
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
|
||
}
|
||
.pip.W { background: var(--mtg-w); color: #4a3f2a; }
|
||
.pip.U { background: var(--mtg-u); color: #fff; }
|
||
.pip.B { background: var(--mtg-b); color: #cbc3d4; }
|
||
.pip.R { background: var(--mtg-r); color: #fff; }
|
||
.pip.G { background: var(--mtg-g); color: #fff; }
|
||
.pip.C { background: var(--mtg-c); color: #23242a; }
|
||
|
||
.color-toggles {
|
||
display: flex;
|
||
gap: 5px;
|
||
}
|
||
.color-toggles .pip {
|
||
cursor: pointer;
|
||
opacity: 0.32;
|
||
transition: opacity 0.1s, box-shadow 0.1s;
|
||
}
|
||
.color-toggles .pip.on {
|
||
opacity: 1;
|
||
box-shadow: 0 0 0 2px var(--accent-hover);
|
||
}
|
||
/* Rarity toggles borrow the colour pips' on/off treatment, tinted like the
|
||
expansion-symbol colours printed on the cards themselves. */
|
||
/* Common's black symbol needs a ring to show at all on the dark panel. */
|
||
.pip.rarity-pip.common { background: #2b2a2e; color: #e6e1d8; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4); }
|
||
.pip.rarity-pip.uncommon { background: linear-gradient(135deg, #c9d3dc, #7f8c98); color: #1d232a; }
|
||
.pip.rarity-pip.rare { background: linear-gradient(135deg, #e6c66e, #a77f2a); color: #2b2210; }
|
||
.pip.rarity-pip.mythic { background: linear-gradient(135deg, #f08a3c, #c2401f); color: #fff; }
|
||
|
||
/* ============================================================
|
||
Tables
|
||
============================================================ */
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
th {
|
||
text-align: left;
|
||
padding: var(--space-2) var(--space-3);
|
||
font-size: var(--fs-label);
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: var(--tracking-label);
|
||
color: var(--muted);
|
||
position: sticky;
|
||
top: 0;
|
||
background: var(--bg-content);
|
||
border-bottom: 1px solid var(--border);
|
||
z-index: 1;
|
||
}
|
||
td {
|
||
padding: 8px 12px;
|
||
border-bottom: 1px solid var(--border-soft);
|
||
vertical-align: middle;
|
||
}
|
||
tbody tr {
|
||
transition: background 0.08s;
|
||
}
|
||
tbody tr:hover {
|
||
background: rgba(255, 255, 255, 0.022);
|
||
}
|
||
|
||
/* On a phone, the List view's 8-column table has no room to lay out
|
||
side-by-side and forced horizontal scrolling of the whole page (not just
|
||
the table) — see the .content padding/overflow rules elsewhere, nothing
|
||
scoped the scroll to the table itself. Below this breakpoint each row
|
||
becomes its own stacked card instead: every cell drops to its own line,
|
||
headers (meaningless once stacked) are hidden, and cells that need a label
|
||
outside their normal column context get one from `data-label` (set in the
|
||
view components — see LibraryBrowser/CardSearch/DeckView's table JSX). */
|
||
@media (max-width: 720px) {
|
||
table,
|
||
tbody,
|
||
tr {
|
||
display: block;
|
||
width: 100%;
|
||
}
|
||
thead {
|
||
display: none;
|
||
}
|
||
table {
|
||
border-collapse: separate;
|
||
border-spacing: 0;
|
||
}
|
||
tbody tr {
|
||
position: relative;
|
||
overflow: hidden; /* contains .cell-thumb's float below */
|
||
background: var(--bg-panel);
|
||
border: 1px solid var(--border-soft);
|
||
border-radius: var(--radius-lg);
|
||
padding: var(--space-3);
|
||
padding-right: calc(var(--space-3) + 28px); /* clears .cell-check */
|
||
margin-bottom: var(--space-3);
|
||
}
|
||
tbody tr:hover {
|
||
background: var(--bg-panel);
|
||
}
|
||
td {
|
||
padding: 0;
|
||
border-bottom: none;
|
||
}
|
||
/* Selection checkbox: pinned to the row-card's corner instead of taking a
|
||
line of its own. */
|
||
.cell-check {
|
||
position: absolute;
|
||
top: var(--space-3);
|
||
right: var(--space-3);
|
||
width: auto;
|
||
}
|
||
/* Thumbnail floats so the name/type text wraps beside it — that's what
|
||
buys back the vertical space the old one-cell-per-line layout wasted. */
|
||
.cell-thumb {
|
||
float: left;
|
||
width: 56px;
|
||
margin-right: var(--space-3);
|
||
}
|
||
.cell-thumb .card-thumb,
|
||
.cell-thumb .thumb-empty {
|
||
display: block;
|
||
width: 56px;
|
||
height: auto;
|
||
}
|
||
td.card-name {
|
||
display: block;
|
||
font-size: var(--fs-body);
|
||
font-weight: 700;
|
||
color: var(--text-strong);
|
||
}
|
||
td.card-type {
|
||
display: block;
|
||
color: var(--muted);
|
||
font-size: var(--fs-caption);
|
||
}
|
||
/* Color pips and any other small cell (MV, Owned, In deck, Decks /
|
||
Location, ...) sit inline as wrapping chips instead of one per line —
|
||
as long as they fit within the floated thumbnail's height they tuck in
|
||
beside it, otherwise they wrap below it like normal text. */
|
||
td:has(.pips),
|
||
td[data-label] {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
margin: var(--space-2) var(--space-3) 0 0;
|
||
font-size: var(--fs-caption);
|
||
}
|
||
td[data-label]::before {
|
||
content: attr(data-label) ":";
|
||
font-size: var(--fs-micro);
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
color: var(--faint);
|
||
}
|
||
/* Buttons (Add to deck, maybeboard toggle, ...) always get their own full
|
||
row below everything else. */
|
||
td:has(button) {
|
||
display: block;
|
||
clear: both;
|
||
margin-top: var(--space-2);
|
||
}
|
||
}
|
||
|
||
.over-assigned {
|
||
color: var(--warn);
|
||
font-weight: 600;
|
||
}
|
||
.deck-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
background: var(--bg-input);
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
padding: 2px 8px;
|
||
font-size: 12px;
|
||
color: var(--text);
|
||
margin-right: 4px;
|
||
}
|
||
|
||
/* ============================================================
|
||
Bulk-selection action bar (Library tab)
|
||
============================================================ */
|
||
|
||
.bulk-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
background: var(--accent-subtle);
|
||
border: 1px solid var(--accent-glow);
|
||
border-radius: var(--radius-lg);
|
||
padding: 10px 16px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.bulk-bar-count {
|
||
font-weight: 700;
|
||
font-size: 13px;
|
||
color: var(--accent-hover);
|
||
white-space: nowrap;
|
||
}
|
||
.bulk-bar-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.bulk-bar-delta {
|
||
width: 48px;
|
||
text-align: center;
|
||
padding: 8px 4px;
|
||
}
|
||
|
||
/* Floating "Undo" snackbar (e.g. after a bulk add-to-deck). Fixed so it stays
|
||
reachable regardless of scroll position; sits below dialogs (z-index 400+)
|
||
since it's only ever shown against the current view, not over a modal. */
|
||
.undo-bar {
|
||
position: fixed;
|
||
left: 50%;
|
||
bottom: 28px;
|
||
transform: translateX(-50%);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
background: var(--bg-panel);
|
||
border: 1px solid var(--border-soft);
|
||
border-radius: var(--radius-lg);
|
||
padding: 10px 16px;
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
||
z-index: 300;
|
||
}
|
||
.undo-bar-close {
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--muted);
|
||
font-size: 16px;
|
||
line-height: 1;
|
||
padding: 2px 6px;
|
||
cursor: pointer;
|
||
}
|
||
.undo-bar-close:hover {
|
||
color: var(--text-strong);
|
||
}
|
||
|
||
/* ============================================================
|
||
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: var(--space-4);
|
||
}
|
||
/* Wraps every field after "Name contains" so it can be hidden as a group on
|
||
narrow screens — a 9-10 field grid is a wall to scroll past on a phone
|
||
before any results appear. `display: contents` on the wrapper (rather than
|
||
e.g. a nested grid) keeps every field a direct child of `.filters` at every
|
||
width, so it doesn't disturb the shared grid's column sizing/auto-flow;
|
||
only the max-width:720px rule below actually hides anything. */
|
||
.filters-more {
|
||
display: contents;
|
||
}
|
||
/* Irrelevant above the breakpoint (every field is already visible there). */
|
||
.filters-toggle {
|
||
display: none;
|
||
}
|
||
@media (max-width: 720px) {
|
||
.filters-toggle {
|
||
display: inline-flex;
|
||
margin-top: var(--space-3);
|
||
}
|
||
.filters-more:not(.expanded) {
|
||
display: none;
|
||
}
|
||
}
|
||
/* 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: var(--fs-caption);
|
||
font-weight: 600;
|
||
color: var(--muted);
|
||
}
|
||
/* Compact icon filter toggles, following .color-toggles' visual grammar: dim
|
||
when off, lit with an accent ring when on. Replaces a stack of labelled
|
||
checkboxes that ate most of the filter panel's height — the explanations
|
||
moved into tooltips. */
|
||
.icon-toggles {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.icon-toggle {
|
||
position: relative;
|
||
width: 28px;
|
||
height: 28px;
|
||
padding: 0;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: var(--radius-sm);
|
||
background: var(--bg-elev);
|
||
color: var(--text);
|
||
opacity: 0.4;
|
||
cursor: pointer;
|
||
transition: opacity 0.1s, box-shadow 0.1s, color 0.1s;
|
||
}
|
||
.icon-toggle:hover {
|
||
opacity: 0.75;
|
||
}
|
||
.icon-toggle.on {
|
||
opacity: 1;
|
||
box-shadow: 0 0 0 2px var(--accent-hover);
|
||
}
|
||
/* Removing cards should not look like selecting them. */
|
||
.icon-toggle.exclude.on {
|
||
color: var(--danger);
|
||
box-shadow: 0 0 0 2px var(--danger);
|
||
}
|
||
/* Diagonal bar across an active exclusion, so "hidden" reads without the
|
||
tooltip. */
|
||
.icon-toggle-slash {
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
background: linear-gradient(
|
||
to bottom right,
|
||
transparent calc(50% - 1px),
|
||
currentColor calc(50% - 1px),
|
||
currentColor calc(50% + 1px),
|
||
transparent calc(50% + 1px)
|
||
);
|
||
}
|
||
/* Separates the exclusions from the selecting filter beside them. */
|
||
.icon-toggles-sep {
|
||
width: 1px;
|
||
align-self: stretch;
|
||
margin: 2px 3px;
|
||
background: var(--border-soft);
|
||
}
|
||
|
||
.location-input {
|
||
padding: 4px 8px;
|
||
font-size: 13px;
|
||
max-width: 160px;
|
||
}
|
||
|
||
/* ============================================================
|
||
TagInput: chip-based multi-value filter with autocomplete
|
||
(type/subtype/keyword filters in the Library tab)
|
||
============================================================ */
|
||
|
||
.tag-input {
|
||
position: relative;
|
||
}
|
||
.tag-chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 5px;
|
||
min-height: 34px;
|
||
padding: 5px 8px;
|
||
background: var(--bg-input);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
transition: border-color 0.12s, box-shadow 0.12s;
|
||
}
|
||
.tag-chips:focus-within {
|
||
border-color: var(--accent);
|
||
box-shadow: 0 0 0 3px var(--accent-subtle);
|
||
}
|
||
.tag-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
background: var(--accent-subtle);
|
||
color: var(--accent-hover);
|
||
border-radius: 999px;
|
||
padding: 2px 4px 2px 10px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
}
|
||
.tag-chip-remove {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 17px;
|
||
height: 17px;
|
||
padding: 0;
|
||
border: none;
|
||
border-radius: 50%;
|
||
background: none;
|
||
color: inherit;
|
||
font-size: 14px;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
}
|
||
.tag-chip-remove:hover {
|
||
background: rgba(0, 0, 0, 0.25);
|
||
}
|
||
.tag-chip-input {
|
||
flex: 1;
|
||
min-width: 70px;
|
||
border: none;
|
||
background: none;
|
||
padding: 3px 2px;
|
||
font-size: 14px;
|
||
}
|
||
.tag-chip-input:focus {
|
||
outline: none;
|
||
border: none;
|
||
box-shadow: none;
|
||
}
|
||
.tag-suggestions {
|
||
position: absolute;
|
||
z-index: 50;
|
||
top: calc(100% + 4px);
|
||
left: 0;
|
||
right: 0;
|
||
max-height: 220px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* ============================================================
|
||
Card images: thumbnails, hover preview, tiles
|
||
============================================================ */
|
||
|
||
.card-thumb {
|
||
width: 40px;
|
||
height: 56px;
|
||
object-fit: cover;
|
||
border-radius: 5px;
|
||
display: block;
|
||
background: var(--bg-elev);
|
||
cursor: zoom-in;
|
||
box-shadow: var(--shadow-sm);
|
||
transition: transform 0.1s, box-shadow 0.1s;
|
||
}
|
||
.card-thumb:hover {
|
||
transform: scale(1.05);
|
||
box-shadow: 0 0 0 2px var(--accent-hover), var(--shadow-md);
|
||
}
|
||
.thumb-empty {
|
||
display: inline-flex;
|
||
width: 40px;
|
||
height: 56px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--faint);
|
||
border: 1px dashed var(--border);
|
||
border-radius: 5px;
|
||
font-size: 19px;
|
||
}
|
||
.hover-card {
|
||
position: fixed;
|
||
/* Above .dialog-backdrop (400) — cards inside a dialog (e.g. the
|
||
suggestions modal) can be hovered for a preview without it appearing
|
||
behind the dialog it's in. */
|
||
z-index: 450;
|
||
border-radius: 14px;
|
||
overflow: hidden;
|
||
box-shadow: var(--shadow-lg);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
pointer-events: none;
|
||
}
|
||
.hover-card-img {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.preview-modal-backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
/* Above .dialog-backdrop (400) — a card can now be clicked to zoom from
|
||
inside a dialog (e.g. the sample hand modal), and the zoomed preview
|
||
must stay on top of the dialog it was opened from. */
|
||
z-index: 450;
|
||
/* Centered via `.preview-modal`'s `margin: auto`, NOT align-items /
|
||
justify-content: center — those overflow equally off BOTH edges when the
|
||
content is taller than the viewport (card art + info tile on a phone),
|
||
and the part pushed above the top edge can't be scrolled to, leaving the
|
||
close button under the status bar. Auto margins collapse to 0 instead,
|
||
so overflow only ever extends downward into the scrollable area. */
|
||
display: flex;
|
||
overflow-y: auto;
|
||
padding: 24px 12px;
|
||
/* The close button pokes up above the (flex-centered) modal content, so on
|
||
a tall/near-fullscreen card image it can land right under the Android
|
||
status bar — same edge-to-edge issue as .topbar, same fix: pad the safe
|
||
area instead of a fixed inset. Degrades to the base 24px on anything
|
||
without a system bar to avoid. */
|
||
padding-top: calc(24px + env(safe-area-inset-top));
|
||
padding-bottom: calc(24px + env(safe-area-inset-bottom));
|
||
background: rgba(10, 10, 12, 0.72);
|
||
backdrop-filter: blur(2px);
|
||
cursor: zoom-out;
|
||
animation: preview-fade-in 0.12s ease;
|
||
}
|
||
@keyframes preview-fade-in {
|
||
from {
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
/* The info tile (see `.preview-info`) sits beside the card art whenever
|
||
there's room, and wraps onto its own line below when there isn't —
|
||
plain flex-wrap rather than a hand-tuned width breakpoint, so neither the
|
||
art nor the info tile ever has to shrink to make the other fit. Any
|
||
remaining vertical overflow (e.g. a double-faced card's full-size faces
|
||
plus a tall info tile on a short window) is handled by
|
||
`.preview-modal-backdrop`'s own scrolling, not by clipping content. */
|
||
.preview-modal {
|
||
position: relative;
|
||
margin: auto;
|
||
cursor: default;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
gap: 16px; /* mirrored by PREVIEW_GAP in components.tsx */
|
||
max-width: 96vw;
|
||
}
|
||
.preview-modal-faces {
|
||
position: relative;
|
||
}
|
||
/* Sized explicitly (Scryfall "normal" images are 488x680) rather than from
|
||
the image's intrinsic size, so the box is already its final size before the
|
||
image loads — ClickPreviewModal measures the layout on mount to decide on
|
||
compact mode. Same result as the old intrinsic sizing capped at 90vw/90vh;
|
||
`--preview-img-h` is the height budget, shrunk in compact mode. */
|
||
.preview-modal {
|
||
--preview-img-h: 90vh;
|
||
}
|
||
.preview-modal-img {
|
||
display: none;
|
||
width: min(488px, 90vw, calc(var(--preview-img-h) * 488 / 680));
|
||
aspect-ratio: 488 / 680;
|
||
max-width: min(720px, 90vw);
|
||
border-radius: 18px;
|
||
box-shadow: var(--shadow-lg);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
}
|
||
.preview-modal-img.active {
|
||
display: block;
|
||
}
|
||
/* Horizontal cards (Battles/Planes/Phenomena, see `CardRef.horizontal`): the
|
||
stored image is portrait-shaped with the content itself rotated. The
|
||
wrapper's width/height (landscape) and the inner img's width/height
|
||
(the same box transposed, pre-rotation) are computed in JS —
|
||
`useRotatedPreviewBox` — and passed in as inline styles; this just
|
||
positions/rotates the img to fill the wrapper exactly. */
|
||
.preview-modal-img-rotated {
|
||
position: relative;
|
||
overflow: hidden;
|
||
/* Override `.preview-modal-img`'s portrait-oriented max-width/max-height —
|
||
they'd otherwise clamp this wrapper's JS-computed (correctly landscape)
|
||
inline width/height down, while the un-clamped rotated img inside stays
|
||
at its full computed size and overflows, getting cut off by the
|
||
`overflow: hidden` above instead of shrinking to fit. */
|
||
max-width: none;
|
||
max-height: none;
|
||
}
|
||
.preview-modal-rotate-img {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%) rotate(90deg);
|
||
}
|
||
/* Wide enough to show a double-faced card's front and back side by side —
|
||
no flip needed. */
|
||
@media (min-width: 860px) {
|
||
.preview-modal-dfc .preview-modal-faces {
|
||
display: flex;
|
||
gap: 20px;
|
||
}
|
||
.preview-modal-dfc .preview-modal-img {
|
||
display: block;
|
||
max-width: min(430px, 43vw);
|
||
}
|
||
/* A horizontal front face (e.g. a transformed Battle) shown side-by-side
|
||
with its back face: keep using the JS-computed inline width/height
|
||
(see `.preview-modal-img-rotated` above) rather than the narrower DFC
|
||
column width, which would clamp the wrapper and crop the rotated image
|
||
the same way the base max-width/max-height would. */
|
||
.preview-modal-dfc .preview-modal-img-rotated {
|
||
max-width: none;
|
||
}
|
||
.preview-modal-dfc .preview-modal-flip {
|
||
display: none;
|
||
}
|
||
}
|
||
/* Compact mode (see ClickPreviewModal): the full layout would be taller than
|
||
the viewport, so it shows one pane at a time — a single face, or the info
|
||
tile — above a toolbar (pane toggle + close) that sits at the bottom for
|
||
thumb reach. The modal's width/height and `--preview-img-h` are set inline
|
||
from JS: a fixed box sized to the largest pane, so the toolbar stays put
|
||
when switching panes. Each pane is pushed down onto the toolbar
|
||
(`margin-top: auto`) and centered horizontally; an info tile taller than
|
||
the art budget scrolls within itself. */
|
||
.preview-modal-compact {
|
||
flex-direction: column;
|
||
flex-wrap: nowrap;
|
||
align-items: stretch;
|
||
}
|
||
.preview-modal-compact .preview-modal-faces,
|
||
.preview-modal-compact .preview-info {
|
||
margin-top: auto;
|
||
align-self: center;
|
||
}
|
||
.preview-modal-compact .preview-modal-faces {
|
||
display: block;
|
||
flex-shrink: 0;
|
||
}
|
||
.preview-modal-compact .preview-info {
|
||
flex: 0 1 auto;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
}
|
||
/* Height mirrored by PREVIEW_BAR_H in components.tsx. */
|
||
.preview-modal-bar {
|
||
flex-shrink: 0;
|
||
height: 36px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
.preview-modal-compact .preview-modal-img:not(.active) {
|
||
display: none;
|
||
}
|
||
.preview-modal-compact .preview-modal-img:not(.preview-modal-img-rotated) {
|
||
max-width: min(720px, 90vw);
|
||
}
|
||
.preview-modal-compact .preview-modal-close,
|
||
.preview-modal-compact .preview-modal-flip,
|
||
.preview-modal-compact:not(.preview-modal-show-info) .preview-info,
|
||
.preview-modal-show-info .preview-modal-faces {
|
||
display: none;
|
||
}
|
||
.preview-modal-close {
|
||
position: absolute;
|
||
top: -14px;
|
||
right: -14px;
|
||
}
|
||
.preview-modal-close .icon-btn {
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
.preview-modal-close .icon-btn:hover {
|
||
background: var(--danger);
|
||
color: #fff;
|
||
}
|
||
.preview-modal-flip {
|
||
position: absolute;
|
||
bottom: 14px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
padding: 7px 16px;
|
||
border-radius: 999px;
|
||
background: rgba(20, 20, 24, 0.75);
|
||
color: #fff;
|
||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
.preview-modal-flip:hover {
|
||
background: var(--accent);
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
.preview-info {
|
||
width: min(320px, 90vw);
|
||
flex: 0 0 auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-3);
|
||
background: var(--bg-panel);
|
||
border: 1px solid var(--border-soft);
|
||
border-radius: var(--radius-lg);
|
||
padding: var(--space-4);
|
||
box-shadow: var(--shadow-lg), var(--panel-highlight);
|
||
color: var(--text);
|
||
}
|
||
.preview-info-name {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
line-height: 1.25;
|
||
color: var(--text-strong);
|
||
}
|
||
.preview-info-type {
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
}
|
||
.preview-info-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.preview-info-mv {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
.preview-info-price {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: var(--gold);
|
||
}
|
||
.preview-info-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
padding-top: 10px;
|
||
border-top: 1px solid var(--border-soft);
|
||
}
|
||
.preview-info-label {
|
||
font-size: var(--fs-label);
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: var(--tracking-label);
|
||
color: var(--muted);
|
||
}
|
||
.preview-info-deck-chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
.preview-info-legal-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
.legal-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
border-radius: 999px;
|
||
padding: 2px 8px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
border: 1px solid transparent;
|
||
white-space: nowrap;
|
||
}
|
||
.legal-pill.legal-legal {
|
||
background: rgba(75, 176, 115, 0.16);
|
||
border-color: rgba(75, 176, 115, 0.4);
|
||
color: var(--success);
|
||
}
|
||
.legal-pill.legal-restricted {
|
||
background: rgba(224, 163, 74, 0.16);
|
||
border-color: rgba(224, 163, 74, 0.4);
|
||
color: var(--warn);
|
||
}
|
||
.legal-pill.legal-banned {
|
||
background: rgba(216, 80, 63, 0.16);
|
||
border-color: rgba(216, 80, 63, 0.4);
|
||
color: var(--danger);
|
||
text-decoration: line-through;
|
||
}
|
||
.legal-pill.legal-not_legal {
|
||
border-color: var(--border-soft);
|
||
color: var(--faint);
|
||
opacity: 0.55;
|
||
}
|
||
|
||
.tile-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
.tile {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 7px;
|
||
}
|
||
/* A rotated (landscape) card at a single portrait column's width renders
|
||
~2x smaller in area than a normal tile (a 85:61 box is short as well as
|
||
narrow) — hard to read. Spanning 2 columns gives it a comparable, legible
|
||
footprint instead. */
|
||
.tile-rotated {
|
||
grid-column: span 2;
|
||
}
|
||
/* …except when the per-row control is down to one column, where a span of 2
|
||
would add an implicit second column and overflow. */
|
||
.tile-grid-single .tile-rotated {
|
||
grid-column: auto;
|
||
}
|
||
.tile-img-wrap {
|
||
position: relative;
|
||
}
|
||
.tile-img {
|
||
width: 100%;
|
||
display: block;
|
||
border-radius: var(--radius-card);
|
||
box-shadow: var(--shadow-sm);
|
||
cursor: zoom-in;
|
||
transition: transform var(--dur-fast), box-shadow var(--dur-fast);
|
||
}
|
||
.tile:hover .tile-img {
|
||
transform: translateY(-3px);
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
/* Horizontal view (see `CardTile`'s `rotated` prop): swaps the tile to a
|
||
landscape footprint via `aspect-ratio` — the width still comes from the
|
||
grid column, so no viewport math is needed here, unlike the click-preview
|
||
modal's rotated case. `container-type: size` lets the actual (still
|
||
portrait, pre-rotation) `<img>` inside reference that computed box via
|
||
`cqw`/`cqh` to size itself as the exact transpose, so rotating it 90° fills
|
||
the wrapper exactly. */
|
||
.tile-img-wrap-rotated {
|
||
position: relative;
|
||
width: 100%;
|
||
aspect-ratio: 85 / 61;
|
||
container-type: size;
|
||
overflow: hidden;
|
||
border-radius: var(--radius-card);
|
||
box-shadow: var(--shadow-sm);
|
||
transition: transform var(--dur-fast), box-shadow var(--dur-fast);
|
||
}
|
||
.tile:hover .tile-img-wrap-rotated {
|
||
transform: translateY(-3px);
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
.tile-img-rotated {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
width: 100cqh;
|
||
height: 100cqw;
|
||
display: block;
|
||
cursor: zoom-in;
|
||
transform: translate(-50%, -50%) rotate(90deg);
|
||
}
|
||
.tile-noimg {
|
||
aspect-ratio: 488 / 680;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-card);
|
||
background: var(--bg-panel);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
padding: 14px;
|
||
font-weight: 600;
|
||
color: var(--muted);
|
||
}
|
||
.tile-corner {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
display: flex;
|
||
gap: 6px;
|
||
/* The card art's own mana cost prints in this corner — only cover it while
|
||
the tile is actually being interacted with. */
|
||
opacity: 0;
|
||
transition: opacity 0.12s;
|
||
}
|
||
.tile:hover .tile-corner,
|
||
.tile:focus-within .tile-corner {
|
||
opacity: 1;
|
||
}
|
||
.tile-corner-left {
|
||
position: absolute;
|
||
top: 8px;
|
||
left: 8px;
|
||
/* Same reasoning as .tile-corner: don't cover the card name unless the
|
||
tile is being interacted with — except once selected, where the
|
||
checkbox should stay visible so the selection is visible at a glance. */
|
||
opacity: 0;
|
||
transition: opacity 0.12s;
|
||
}
|
||
.tile:hover .tile-corner-left,
|
||
.tile:focus-within .tile-corner-left,
|
||
.tile-selected .tile-corner-left {
|
||
opacity: 1;
|
||
}
|
||
.tile-corner-bottom-right {
|
||
position: absolute;
|
||
bottom: 8px;
|
||
right: 8px;
|
||
opacity: 0;
|
||
transition: opacity 0.12s;
|
||
}
|
||
.tile:hover .tile-corner-bottom-right,
|
||
.tile:focus-within .tile-corner-bottom-right {
|
||
opacity: 1;
|
||
}
|
||
.tile-corner-bottom-left {
|
||
position: absolute;
|
||
bottom: 8px;
|
||
left: 8px;
|
||
opacity: 0;
|
||
transition: opacity 0.12s;
|
||
}
|
||
.tile:hover .tile-corner-bottom-left,
|
||
.tile:focus-within .tile-corner-bottom-left {
|
||
opacity: 1;
|
||
}
|
||
.tile-selected .tile-img {
|
||
box-shadow: 0 0 0 3px var(--accent), var(--shadow-md);
|
||
}
|
||
.tile-select {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 26px;
|
||
height: 26px;
|
||
border-radius: 999px;
|
||
background: rgba(0, 0, 0, 0.78);
|
||
backdrop-filter: blur(2px);
|
||
}
|
||
.tile-badge {
|
||
background: rgba(0, 0, 0, 0.78);
|
||
color: #fff;
|
||
border-radius: 8px;
|
||
padding: 2px 9px;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
backdrop-filter: blur(2px);
|
||
}
|
||
.tile-badge.warn {
|
||
background: var(--warn);
|
||
color: #241700;
|
||
}
|
||
.tile-footer {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
align-items: center;
|
||
}
|
||
/* Inputs keep an intrinsic ~160px width that would overflow tiles narrowed
|
||
by the per-row control; let them shrink with the tile instead. */
|
||
.tile-footer input {
|
||
min-width: 0;
|
||
}
|
||
|
||
/* ============================================================
|
||
Toolbar controls: group-by + view toggle
|
||
============================================================ */
|
||
|
||
.group-by {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
font-size: 13px;
|
||
color: var(--muted);
|
||
}
|
||
.sort-dir-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
width: 30px;
|
||
height: 30px;
|
||
padding: 0;
|
||
font-size: 15px;
|
||
}
|
||
.view-toggle {
|
||
display: inline-flex;
|
||
background: var(--bg-input);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
}
|
||
.view-toggle button {
|
||
border: none;
|
||
border-radius: 0;
|
||
background: transparent;
|
||
font-size: 13px;
|
||
padding: 5px 12px;
|
||
color: var(--muted);
|
||
}
|
||
.view-toggle button:hover {
|
||
background: rgba(255, 255, 255, 0.04);
|
||
}
|
||
.view-toggle button.active {
|
||
background: var(--accent);
|
||
color: #fff;
|
||
}
|
||
.view-toggle button:disabled {
|
||
background: transparent;
|
||
opacity: 0.4;
|
||
cursor: default;
|
||
}
|
||
/* Tiles-per-row stepper (see `TileColumnsControl`): same segmented look as
|
||
the List/Tiles toggle, with a fixed-width count between − and +. */
|
||
.tile-columns button {
|
||
padding: 5px 9px;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.tile-columns .tile-columns-count {
|
||
min-width: 2.2em;
|
||
padding-left: 4px;
|
||
padding-right: 4px;
|
||
}
|
||
.tile-columns .tile-columns-count:disabled {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* ============================================================
|
||
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;
|
||
}
|
||
.deck-detail-pane {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* Surface from `.panel`; layout only here. */
|
||
.deck-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: flex-start;
|
||
gap: var(--space-5);
|
||
}
|
||
/* Separates the deck-level info panels (commander, format, analysis,
|
||
legality) from the card-management controls + lists below. */
|
||
.deck-section-divider {
|
||
border: none;
|
||
border-top: 1px solid var(--border-soft);
|
||
margin: 4px 0 18px;
|
||
}
|
||
|
||
/* The four top-of-page info panels (commander, meta, analysis, legality).
|
||
Single column matches the existing top-to-bottom reading order on
|
||
narrow/portrait viewports; on wide viewports the compact panels move
|
||
into a side column so the analysis panel (the one panel that benefits
|
||
from extra width) gets the rest of the row instead of everything
|
||
stretching out with wasted horizontal space. grid-template-areas (not
|
||
flex wrapping) is what lets the two-column arrangement reorder visually
|
||
without touching DOM order. */
|
||
.deck-top-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
grid-template-areas:
|
||
"commander"
|
||
"meta"
|
||
"stats"
|
||
"legality";
|
||
gap: 16px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.deck-top-commander {
|
||
grid-area: commander;
|
||
}
|
||
.deck-top-meta {
|
||
grid-area: meta;
|
||
}
|
||
.deck-top-stats {
|
||
grid-area: stats;
|
||
}
|
||
.deck-top-legality {
|
||
grid-area: legality;
|
||
}
|
||
@media (min-width: 1100px) {
|
||
.deck-top-grid {
|
||
grid-template-columns: minmax(360px, 560px) 1fr;
|
||
grid-template-areas:
|
||
"commander stats"
|
||
"meta stats"
|
||
"legality stats";
|
||
align-items: start;
|
||
}
|
||
}
|
||
.commander-box {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
min-width: 280px;
|
||
flex: 1;
|
||
}
|
||
|
||
/* 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: var(--space-4);
|
||
}
|
||
.commander-hero-img {
|
||
width: 84px;
|
||
height: 117px;
|
||
flex-shrink: 0;
|
||
object-fit: cover;
|
||
border-radius: 8px;
|
||
cursor: zoom-in;
|
||
}
|
||
.commander-hero-noimg {
|
||
width: 84px;
|
||
height: 117px;
|
||
flex-shrink: 0;
|
||
border: 1px dashed var(--border);
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
padding: 8px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: var(--muted);
|
||
}
|
||
.commander-hero-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
}
|
||
.commander-hero-label {
|
||
font-size: var(--fs-micro);
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
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: var(--fs-display);
|
||
font-weight: 700;
|
||
color: var(--text-strong);
|
||
}
|
||
.commander-hero-type {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
/* Per-deck analysis panel. Surface from `.panel`. */
|
||
.deck-stats-head {
|
||
margin-bottom: var(--space-3);
|
||
}
|
||
.deck-stats-body {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 24px;
|
||
}
|
||
.deck-stats-numbers {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
min-width: 220px;
|
||
}
|
||
.deck-stats-curve {
|
||
flex: 1;
|
||
min-width: 220px;
|
||
max-width: 420px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.deck-stats-curve-label {
|
||
font-size: var(--fs-micro);
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
color: var(--faint);
|
||
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%;
|
||
}
|
||
}
|
||
|
||
/* Back button into the deck list — only meaningful once the list and the
|
||
deck-building pane become separate screens below, so it's hidden outside
|
||
that breakpoint (and on the tablet stacked layout, where both panes are
|
||
still visible together). */
|
||
.deck-back-btn {
|
||
display: none;
|
||
}
|
||
|
||
/* Compact: tighten page gutters and collapse the nav rail to an icon strip
|
||
the left rail's job moves to .bottom-nav below instead, the customary
|
||
placement on Android). */
|
||
@media (max-width: 720px) {
|
||
.content {
|
||
padding: var(--space-4);
|
||
/* Clear the fixed bottom nav, or its last row of content is hidden
|
||
behind it — the safe-area inset is already baked into the nav's own
|
||
height, so it isn't added again here. */
|
||
padding-bottom: calc(var(--bottom-nav-h) + env(safe-area-inset-bottom) + var(--space-4));
|
||
}
|
||
.nav-rail {
|
||
display: none;
|
||
}
|
||
.bottom-nav {
|
||
display: flex;
|
||
}
|
||
.topbar {
|
||
padding-left: var(--space-3);
|
||
padding-right: var(--space-3);
|
||
/* padding-top (the safe-area inset) is untouched here — this must not
|
||
reintroduce the shorthand `padding: 0 ...` that was zeroing it out. */
|
||
}
|
||
/* The library path is the first thing to go when horizontal space is tight —
|
||
the library actions stay reachable. */
|
||
.path {
|
||
display: none;
|
||
}
|
||
/* Deck view's header (name + Suggest/Export/Sample buttons) and several
|
||
panels below assume desktop width via `min-width` (280/220px) and a
|
||
non-wrapping `.row` — those don't fit a phone and forced the whole page
|
||
to scroll horizontally. Let everything wrap and drop the min-widths
|
||
instead. */
|
||
.row {
|
||
flex-wrap: wrap;
|
||
}
|
||
.commander-box,
|
||
.deck-stats-numbers,
|
||
.deck-stats-curve {
|
||
min-width: 0;
|
||
}
|
||
/* `.deck-meta` puts the Format selector and the commander search box side
|
||
by side as flex-wrap row items — with `.commander-box`'s min-width now
|
||
0, it no longer wraps onto its own line, it just gets squeezed into
|
||
whatever sliver is left next to "Format", crushing the search results.
|
||
Stack them instead. */
|
||
.deck-meta {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
/* Deck list and deck-building pane become separate screens (only one
|
||
visible at a time) instead of stacking above one another — with many
|
||
decks, scrolling past the whole list to reach the builder doesn't
|
||
scale. `.deck-layout`'s show-list/show-detail class is toggled by
|
||
DeckView in response to picking a deck / the back button. */
|
||
.deck-back-btn {
|
||
display: inline-flex;
|
||
}
|
||
.deck-layout.show-list .deck-detail-pane {
|
||
display: none;
|
||
}
|
||
.deck-layout.show-detail .deck-sidebar {
|
||
display: none;
|
||
}
|
||
/* The Suggested Cards dialog: `.suggestion-actions`' two nowrap buttons
|
||
are flex-shrink:0, so they claimed their full fixed width no matter how
|
||
little room was left, crushing `.suggestion-main` down to almost
|
||
nothing — the name text then wrapped one word (sometimes one syllable)
|
||
per line instead of actually gaining any space. `flex-basis: 100%` on
|
||
`.suggestion-actions` forces it onto its own full-width line below the
|
||
thumbnail + name/reasons instead of competing with them for the same
|
||
line. */
|
||
.dialog-wide {
|
||
width: 96vw;
|
||
padding: 14px;
|
||
}
|
||
.suggestion-row {
|
||
flex-wrap: wrap;
|
||
}
|
||
.suggestion-head {
|
||
flex-wrap: wrap;
|
||
row-gap: 2px;
|
||
}
|
||
.suggestion-actions {
|
||
flex-basis: 100%;
|
||
flex-direction: row;
|
||
margin-top: 6px;
|
||
}
|
||
.suggestion-actions button {
|
||
flex: 1;
|
||
}
|
||
/* Clear the bottom nav the same way .content does, so the undo snackbar
|
||
doesn't land underneath it. */
|
||
.undo-bar {
|
||
bottom: calc(var(--bottom-nav-h) + env(safe-area-inset-bottom) + var(--space-3));
|
||
}
|
||
/* The ingest bar's segmented layout assumes room for 4 side-by-side
|
||
stage labels — on a phone-width screen the tiny-weighted stages
|
||
("Index", "Indexing") can't fit theirs and it collides with its
|
||
neighbor's. Drop the per-segment labels (the header message above
|
||
already names the current stage in more detail) and let that header
|
||
message wrap instead of ellipsis-truncating, so status text like a
|
||
download's running byte count stays readable. */
|
||
.ingest-bar {
|
||
padding: var(--space-2) var(--space-3) var(--space-3);
|
||
}
|
||
.ingest-bar-message {
|
||
white-space: normal;
|
||
overflow: visible;
|
||
text-overflow: clip;
|
||
}
|
||
.ingest-segment-label {
|
||
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;
|
||
}
|
||
}
|
||
|
||
|
||
/* ============================================================
|
||
News
|
||
============================================================ */
|
||
|
||
/* Releases and articles side by side on desktop; stacked once there isn't
|
||
room, which the ≤900px rule below handles. */
|
||
.news-columns {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: var(--space-5);
|
||
align-items: start;
|
||
}
|
||
@media (max-width: 900px) {
|
||
.news-columns {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
.news-col {
|
||
min-width: 0;
|
||
}
|
||
.news-col .group-header {
|
||
margin-top: 0;
|
||
}
|
||
|
||
/* Both feeds are dense, uniform rows rather than the default result-list's
|
||
roomier search-result spacing. */
|
||
.set-list li,
|
||
.article-list li {
|
||
align-items: center;
|
||
gap: var(--space-3);
|
||
padding: 8px 12px;
|
||
}
|
||
|
||
/* --- releases ------------------------------------------------------------
|
||
A fixed-width date gutter is what makes this scan as a timeline: the dates
|
||
form one column and every set name starts at the same x, regardless of how
|
||
long the date renders in the user's locale. */
|
||
.set-date {
|
||
flex: 0 0 52px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
line-height: 1.15;
|
||
}
|
||
.set-date-day {
|
||
font-size: var(--fs-caption);
|
||
font-weight: 700;
|
||
color: var(--text-strong);
|
||
white-space: nowrap;
|
||
}
|
||
.set-date-year {
|
||
font-size: var(--fs-micro);
|
||
color: var(--faint);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
/* Not-yet-released sets get the accent, so "what's coming" is findable at a
|
||
glance without reading any dates. */
|
||
.set-row.upcoming .set-date-day {
|
||
color: var(--accent-hover);
|
||
}
|
||
|
||
/* Scryfall's set symbols are SVGs with dark ink baked in, so they disappear
|
||
against a dark panel — inverted rather than shipping our own icon set. */
|
||
.set-icon {
|
||
width: 24px;
|
||
height: 24px;
|
||
flex-shrink: 0;
|
||
filter: invert(1);
|
||
opacity: 0.62;
|
||
transition: opacity 0.12s;
|
||
}
|
||
.set-row:hover .set-icon {
|
||
opacity: 0.95;
|
||
}
|
||
.set-row-main {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1px;
|
||
min-width: 0;
|
||
}
|
||
.set-name {
|
||
font-weight: 600;
|
||
color: var(--text-strong);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.set-row-meta {
|
||
font-size: var(--fs-micro);
|
||
color: var(--muted);
|
||
text-transform: capitalize;
|
||
}
|
||
|
||
/* --- articles ------------------------------------------------------------ */
|
||
|
||
/* Fixed width so every title starts at the same x. Variable-width chips left
|
||
the titles ragged, which read as misalignment rather than as structure.
|
||
The categories are a small closed set ("announcements" is the longest),
|
||
so a fixed slot is safe. */
|
||
.article-cat {
|
||
flex: 0 0 88px;
|
||
font-size: var(--fs-micro);
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
color: var(--faint);
|
||
text-align: right;
|
||
line-height: 1.3;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.article-list li:hover .article-cat {
|
||
color: var(--accent-quiet);
|
||
}
|
||
.article-title {
|
||
font-weight: 600;
|
||
color: var(--text-strong);
|
||
line-height: 1.35;
|
||
overflow-wrap: anywhere;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* Suggestion count inside a "narrow to" chip — how many suggestions that
|
||
specific tribe/keyword/archetype explains, so the most useful narrowing is
|
||
obvious before clicking. */
|
||
.filter-chip-count {
|
||
opacity: 0.55;
|
||
font-variant-numeric: tabular-nums;
|
||
margin-left: 2px;
|
||
}
|
||
|
||
/* ============================================================
|
||
Game night (life counter)
|
||
============================================================ */
|
||
|
||
.game-setup {
|
||
max-width: 720px;
|
||
}
|
||
.game-resume {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: var(--space-3);
|
||
flex-wrap: wrap;
|
||
margin-bottom: var(--space-4);
|
||
}
|
||
.game-setup-form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-4);
|
||
}
|
||
.game-seats {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.game-seat {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.game-seat-color {
|
||
width: 28px;
|
||
height: 28px;
|
||
flex-shrink: 0;
|
||
padding: 0;
|
||
border-radius: 50%;
|
||
border: 2px solid rgba(255, 255, 255, 0.15);
|
||
}
|
||
.game-seat-name,
|
||
.game-seat-deck {
|
||
flex: 1 1 120px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.game-board {
|
||
position: relative;
|
||
height: 100%;
|
||
min-height: 420px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
}
|
||
.game-row {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
.game-player {
|
||
position: relative;
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
border-radius: var(--radius-lg);
|
||
border: 2px solid var(--player-color);
|
||
background-color: var(--bg-panel);
|
||
background-color: color-mix(in srgb, var(--player-color) 28%, var(--bg-panel));
|
||
background-size: cover;
|
||
background-position: center 30%;
|
||
color: var(--text-strong);
|
||
/* Lets the life total size itself to the panel (cq units) — a 6-player
|
||
phone layout has panels a third the size of a 2-player one. */
|
||
container-type: size;
|
||
}
|
||
/* Darkens commander art enough for the numbers to read over it. */
|
||
.game-player::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.55));
|
||
pointer-events: none;
|
||
}
|
||
.game-player.rotated {
|
||
transform: rotate(180deg);
|
||
}
|
||
.game-player.first {
|
||
box-shadow: 0 0 0 3px var(--warn), 0 0 24px rgba(224, 163, 74, 0.45);
|
||
}
|
||
/* Dims the panel rather than filtering it, which would also grey out the
|
||
"out" reason and the first-player highlight that need to stay legible. */
|
||
.game-player.out::before {
|
||
background: rgba(12, 11, 10, 0.78);
|
||
}
|
||
.game-player.out .game-life {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.game-life-half {
|
||
position: absolute;
|
||
top: 0;
|
||
bottom: 0;
|
||
z-index: 1;
|
||
width: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0;
|
||
border: none;
|
||
border-radius: 0;
|
||
background: transparent;
|
||
color: rgba(255, 255, 255, 0.35);
|
||
font-size: 28px;
|
||
font-weight: 300;
|
||
touch-action: manipulation;
|
||
}
|
||
.game-life-half:hover {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
}
|
||
.game-life-half:active {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
.game-life-half.minus {
|
||
left: 0;
|
||
justify-content: flex-start;
|
||
padding-left: 8%;
|
||
}
|
||
.game-life-half.plus {
|
||
right: 0;
|
||
justify-content: flex-end;
|
||
padding-right: 8%;
|
||
}
|
||
|
||
.game-player-head {
|
||
position: absolute;
|
||
top: 8px;
|
||
left: 12px;
|
||
right: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
pointer-events: none;
|
||
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.7);
|
||
}
|
||
.game-player-name {
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
}
|
||
.game-player-deck {
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-size: 11px;
|
||
opacity: 0.8;
|
||
}
|
||
.game-life {
|
||
position: relative;
|
||
font-size: 64px;
|
||
font-size: min(34cqw, 44cqh);
|
||
font-weight: 800;
|
||
line-height: 1;
|
||
font-variant-numeric: tabular-nums;
|
||
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.6);
|
||
pointer-events: none;
|
||
}
|
||
.game-delta {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 100%;
|
||
margin-left: 6px;
|
||
padding: 1px 7px;
|
||
border-radius: var(--radius-pill);
|
||
background: rgba(0, 0, 0, 0.5);
|
||
font-size: 16px;
|
||
font-size: max(13px, min(8cqw, 11cqh));
|
||
font-weight: 700;
|
||
text-shadow: none;
|
||
}
|
||
.game-delta.up {
|
||
color: #86e0a4;
|
||
}
|
||
.game-delta.down {
|
||
color: #ff9a86;
|
||
}
|
||
.game-out-label {
|
||
position: relative;
|
||
margin-top: 6px;
|
||
padding: 2px 10px;
|
||
border-radius: var(--radius-pill);
|
||
background: var(--danger);
|
||
color: #fff;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.06em;
|
||
text-transform: uppercase;
|
||
pointer-events: none;
|
||
}
|
||
.game-player-foot {
|
||
position: absolute;
|
||
bottom: 8px;
|
||
left: 8px;
|
||
right: 8px;
|
||
z-index: 2;
|
||
display: flex;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
/* Taps between the badges still reach the −/+ halves underneath. */
|
||
pointer-events: none;
|
||
}
|
||
.game-player-foot > button {
|
||
pointer-events: auto;
|
||
}
|
||
.game-badge {
|
||
padding: 3px 9px;
|
||
border-radius: var(--radius-pill);
|
||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||
background: rgba(0, 0, 0, 0.5);
|
||
color: var(--text-strong);
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
}
|
||
.game-badge-btn:hover {
|
||
background: rgba(0, 0, 0, 0.7);
|
||
}
|
||
.game-badge.first {
|
||
border-color: transparent;
|
||
background: var(--warn);
|
||
color: #2b1d05;
|
||
}
|
||
|
||
.game-strip {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.game-hub {
|
||
width: 36px;
|
||
height: 36px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0;
|
||
border-radius: 50%;
|
||
border: 1px solid var(--border-soft);
|
||
background: var(--bg-elev);
|
||
color: var(--text-strong);
|
||
}
|
||
.game-hub:hover {
|
||
background: var(--bg-hover);
|
||
}
|
||
|
||
.game-menu {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.game-menu button {
|
||
width: 100%;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.game-counters {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
.game-counter-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
}
|
||
.game-counter-heading {
|
||
margin-top: 6px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
.game-dot {
|
||
display: inline-block;
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
}
|
||
.game-stepper {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.game-stepper button {
|
||
min-width: 34px;
|
||
justify-content: center;
|
||
padding: 4px 8px;
|
||
}
|
||
.game-stepper-value {
|
||
min-width: 34px;
|
||
text-align: center;
|
||
font-weight: 700;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.game-dice-buttons {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 6px;
|
||
}
|
||
.game-dice-buttons button {
|
||
justify-content: center;
|
||
}
|
||
.game-dice-result {
|
||
min-height: 96px;
|
||
margin: 12px 0 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.game-dice-latest {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
animation: game-dice-pop 0.22s ease-out;
|
||
}
|
||
.game-dice-value {
|
||
font-size: 52px;
|
||
font-weight: 800;
|
||
line-height: 1.05;
|
||
color: var(--text-strong);
|
||
}
|
||
.game-dice-history {
|
||
font-size: 12px;
|
||
text-align: center;
|
||
}
|
||
@keyframes game-dice-pop {
|
||
from {
|
||
transform: scale(0.6);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: none;
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
/* On a phone the table takes the whole screen — topbar and bottom nav
|
||
included; the hub menu's "Leave the table" (or back) is the way out. */
|
||
@media (max-width: 720px) {
|
||
.game-board {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 300;
|
||
height: auto;
|
||
min-height: 0;
|
||
padding: calc(env(safe-area-inset-top) + 6px) 6px calc(env(safe-area-inset-bottom) + 6px);
|
||
background: var(--bg-app);
|
||
}
|
||
.game-life-half {
|
||
font-size: 22px;
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
Printings: the click preview's per-printing controls, and the picker
|
||
============================================================ */
|
||
.printing-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
min-width: 0;
|
||
}
|
||
.printing-row-thumb {
|
||
width: 28px;
|
||
height: 39px;
|
||
flex-shrink: 0;
|
||
object-fit: cover;
|
||
border-radius: 3px;
|
||
background: var(--bg-elev);
|
||
}
|
||
.printing-row-label {
|
||
min-width: 0;
|
||
font-size: 13px;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.printing-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
.printing-actions button {
|
||
padding: 2px 10px;
|
||
font-size: 12px;
|
||
}
|
||
/* Above the click preview (450) it opens from. */
|
||
.printing-picker-backdrop {
|
||
z-index: 500;
|
||
}
|
||
.printing-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(min(130px, 40vw), 1fr));
|
||
gap: var(--space-3);
|
||
}
|
||
.printing-option {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
}
|
||
.printing-option-art {
|
||
display: block;
|
||
width: 100%;
|
||
padding: 0;
|
||
border: 2px solid transparent;
|
||
border-radius: var(--radius-card);
|
||
background: none;
|
||
cursor: pointer;
|
||
}
|
||
.printing-option-art:hover {
|
||
border-color: var(--accent-hover);
|
||
background: none;
|
||
}
|
||
.printing-option.selected .printing-option-art {
|
||
border-color: var(--accent);
|
||
}
|
||
.printing-option-art img {
|
||
display: block;
|
||
width: 100%;
|
||
aspect-ratio: 488 / 680;
|
||
object-fit: cover;
|
||
border-radius: calc(var(--radius-card) - 2px);
|
||
}
|
||
.printing-option-label {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--text-strong);
|
||
}
|
||
.printing-option-meta {
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
/* Library switcher: the top bar's library label, as a menu button. Keeps
|
||
`.path`'s look (and its hiding on narrow screens). */
|
||
.library-switcher {
|
||
position: relative;
|
||
overflow: visible;
|
||
padding: 0;
|
||
display: flex;
|
||
}
|
||
@media (max-width: 720px) {
|
||
/* Like `.path`: the Info tab's library list stands in for it on phones. */
|
||
.library-switcher {
|
||
display: none;
|
||
}
|
||
}
|
||
.library-switcher-button {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
background: none;
|
||
border: none;
|
||
color: inherit;
|
||
font: inherit;
|
||
padding: 4px 10px;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
}
|
||
.library-switcher-button:hover {
|
||
color: var(--text);
|
||
}
|
||
.library-switcher-label {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.library-menu {
|
||
position: absolute;
|
||
top: calc(100% + var(--space-1));
|
||
left: 0;
|
||
z-index: 50;
|
||
min-width: 280px;
|
||
max-width: min(420px, 90vw);
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: var(--space-1);
|
||
background: var(--bg-elev);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow-md);
|
||
font-family: inherit;
|
||
white-space: normal;
|
||
}
|
||
.library-menu > button {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: var(--space-2);
|
||
background: none;
|
||
border: none;
|
||
border-radius: var(--radius-sm);
|
||
padding: var(--space-2);
|
||
color: var(--text);
|
||
text-align: left;
|
||
font-family: system-ui, sans-serif;
|
||
}
|
||
.library-menu > button:hover {
|
||
background: var(--bg-hover);
|
||
}
|
||
.library-menu hr {
|
||
border: none;
|
||
border-top: 1px solid var(--border);
|
||
margin: var(--space-1) 0;
|
||
width: 100%;
|
||
}
|
||
.library-menu-check {
|
||
width: 1em;
|
||
flex-shrink: 0;
|
||
color: var(--accent-quiet);
|
||
}
|
||
.library-menu-name {
|
||
font-weight: 600;
|
||
color: var(--text-strong);
|
||
}
|
||
.library-menu .hint {
|
||
margin-left: auto;
|
||
padding-left: var(--space-3);
|
||
}
|
||
.library-menu-error {
|
||
padding: var(--space-2);
|
||
font-family: system-ui, sans-serif;
|
||
}
|
||
|
||
/* The Info tab's list of libraries. */
|
||
.library-list {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-2);
|
||
}
|
||
.library-list li {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-3);
|
||
flex-wrap: wrap;
|
||
padding: var(--space-2) var(--space-3);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
}
|
||
.library-list li.active {
|
||
border-color: var(--accent-quiet);
|
||
}
|
||
.library-list-main {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
min-width: 0;
|
||
flex: 1 1 160px;
|
||
}
|
||
.library-list-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
flex-wrap: wrap;
|
||
}
|
||
.library-warn {
|
||
color: var(--warn);
|
||
}
|
||
.library-sync-check {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
cursor: pointer;
|
||
}
|