Vellum/web/src/app.css
mithral 3d964e6b06 Design system v2, and a rotating featured carousel
The skeleton, not features. Two things felt rushed: the colour had no point of
view, and type and shape came from ad-hoc values.

Design language
- Palette reworked to warm-neutral dark with a single muted gold accent
  (#d9a441). The chrome is deliberately colourless because the artwork is the
  colour in this app; gold carries emphasis without competing with covers.
  Light mode re-tuned to match.
- A real type scale (--fs-xs … --fs-3xl) replaces the ad-hoc rem values, with
  fixed line heights and letter-spacing per level. Headings are 620 weight and
  tighter; body is a consistent 15px.
- Radii tightened to a 5/8/12 scale, one shadow pair, and the primary button
  now uses dark ink on gold for contrast.

Featured carousel
- The banner rotates through up to six items — in progress first, then most
  viewed, then newest — drawn only from the libraries the user was granted, so
  it can never show something they cannot open.
- Auto-advances every 8s, pauses on hover and focus, and has dots plus arrows
  for manual control. Slides crossfade, and the ambient wash changes with them.

Verified in the built bundle: accent resolves to #d9a441, six slides, rotation
confirmed across three items, and hover pauses it.
2026-10-10 17:27:59 -04:00

435 lines
11 KiB
CSS

/* Vellum — design tokens.
The artwork is the colour in this app, so the chrome stays warm-neutral and
one muted gold carries emphasis. Type, spacing and radii come from a fixed
scale rather than ad-hoc values, which is what keeps it from feeling rushed. */
:root {
/* ── Surfaces ─────────────────────────────────────────────────────── */
--bg: #0c0c0e;
--surface: #141416;
--surface-2: #1b1b1e;
--surface-3: #232327;
--border: #26262a;
--border-strong: #343439;
/* ── Ink ──────────────────────────────────────────────────────────── */
--text: #f4f3f5;
--muted: #a3a1a9;
--faint: #6d6b75;
/* ── Emphasis ─────────────────────────────────────────────────────── */
--accent: #d9a441;
--accent-hover: #e6b556;
--accent-soft: color-mix(in srgb, var(--accent) 18%, transparent);
--accent-text: #1a1204;
--danger: #e0625c;
--success: #4fbf83;
/* ── Type scale (rem, relative to the 15px root) ──────────────────── */
--fs-xs: 0.8rem; /* 12 */
--fs-sm: 0.867rem; /* 13 */
--fs-md: 0.933rem; /* 14 */
--fs-base: 1rem; /* 15 */
--fs-lg: 1.133rem; /* 17 */
--fs-xl: 1.333rem; /* 20 */
--fs-2xl: 1.733rem; /* 26 */
--fs-3xl: 2.4rem; /* 36 */
--lh-tight: 1.15;
--lh-snug: 1.35;
--lh-body: 1.55;
/* ── Space ────────────────────────────────────────────────────────── */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 24px;
--space-6: 32px;
--space-7: 48px;
/* ── Shape ────────────────────────────────────────────────────────── */
--radius-sm: 5px;
--radius: 8px;
--radius-lg: 12px;
--radius-cover: 8px;
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Text', 'Segoe UI',
Roboto, Helvetica, Arial, 'Noto Sans', sans-serif;
--mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
--nav-width: 236px;
--shadow-cover: 0 1px 2px rgba(0, 0, 0, 0.45), 0 6px 20px rgba(0, 0, 0, 0.35);
--shadow-lift: 0 10px 28px rgba(0, 0, 0, 0.5);
/* User-controlled sizing. --ui-scale drives every rem in the app, so the
text-size setting scales headings, labels and body together. --card-scale
drives cover widths in shelves and grids, so card size is independent of
text size. Both are set from the Appearance panel and persisted locally. */
--ui-scale: 1;
--card-scale: 1;
--card-w: calc(160px * var(--card-scale));
--grid-min: calc(158px * var(--card-scale));
/* Motion. Short, eased, and purposeful: things respond to you, nothing
bounces. Every transition uses one of these so the app moves as one. */
--ease: cubic-bezier(0.22, 1, 0.36, 1);
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
--t-fast: 120ms;
--t-base: 200ms;
--t-slow: 320ms;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #faf9f7;
--surface: #ffffff;
--surface-2: #f3f2ef;
--surface-3: #e9e7e3;
--border: #e6e4df;
--border-strong: #d3d0ca;
--text: #17161a;
--muted: #5f5d66;
--faint: #8a8791;
--accent: #a8761c;
--accent-hover: #8f6414;
--accent-soft: color-mix(in srgb, var(--accent) 14%, transparent);
--accent-text: #ffffff;
--danger: #c0392b;
--success: #1f8a4c;
--shadow-cover: 0 1px 2px rgba(20, 20, 30, 0.08), 0 6px 18px rgba(20, 20, 30, 0.1);
--shadow-lift: 0 10px 26px rgba(20, 20, 30, 0.16);
}
}
* {
box-sizing: border-box;
}
html,
body,
#app {
height: 100%;
margin: 0;
}
html {
/* The single root size every rem is relative to. 15px is a deliberate ~7%
increase over the previous 14px baseline; the Appearance panel multiplies
it via --ui-scale. */
font-size: calc(15px * var(--ui-scale));
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--font);
font-size: var(--fs-base);
line-height: var(--lh-body);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
h1,
h2,
h3 {
margin: 0;
font-weight: 620;
letter-spacing: -0.02em;
line-height: var(--lh-tight);
}
h1 {
font-size: var(--fs-2xl);
}
h2 {
font-size: var(--fs-lg);
letter-spacing: -0.012em;
}
h3 {
font-size: var(--fs-base);
font-weight: 600;
letter-spacing: -0.006em;
}
button,
input,
select {
font: inherit;
color: inherit;
}
/* Keyboard focus is visible everywhere; pointer clicks stay clean. */
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 3px;
}
/* Anyone who has asked the system for less motion gets none of it. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* ── Controls ───────────────────────────────────────────────────────── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: 8px 14px;
border: 1px solid var(--border-strong);
border-radius: var(--radius);
background: var(--surface-2);
color: var(--text);
font-size: var(--fs-md);
font-weight: 500;
cursor: pointer;
transition:
background var(--t-fast) var(--ease),
border-color var(--t-fast) var(--ease),
color var(--t-fast) var(--ease);
white-space: nowrap;
}
.btn:hover:not(:disabled) {
background: var(--surface-3);
border-color: var(--faint);
}
.btn:active:not(:disabled) {
transform: translateY(1px);
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.btn-primary {
background: var(--accent);
border-color: var(--accent);
color: var(--accent-text);
font-weight: 600;
}
.btn-primary:hover:not(:disabled) {
background: var(--accent-hover);
border-color: var(--accent-hover);
}
.btn-ghost {
border-color: transparent;
background: transparent;
}
.btn-ghost:hover:not(:disabled) {
background: var(--surface-2);
border-color: transparent;
}
.btn-lg {
padding: 10px 20px;
font-size: var(--fs-base);
border-radius: var(--radius-lg);
}
.input {
width: 100%;
padding: 9px 12px;
border: 1px solid var(--border-strong);
border-radius: var(--radius);
background: var(--bg);
color: var(--text);
font-size: var(--fs-md);
transition:
border-color var(--t-fast) var(--ease),
box-shadow var(--t-fast) var(--ease);
}
.input::placeholder {
color: var(--faint);
}
.input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.field label {
font-size: var(--fs-sm);
font-weight: 500;
color: var(--muted);
}
/* ── Surfaces ───────────────────────────────────────────────────────── */
.card {
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--surface);
}
.badge {
display: inline-block;
padding: 2px 7px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-strong);
font-size: var(--fs-xs);
color: var(--muted);
}
.chip {
padding: 6px 13px;
border: 1px solid var(--border-strong);
border-radius: var(--radius);
background: transparent;
color: var(--muted);
cursor: pointer;
font-size: var(--fs-md);
transition:
background var(--t-fast) var(--ease),
color var(--t-fast) var(--ease),
border-color var(--t-fast) var(--ease);
}
.chip:hover {
color: var(--text);
border-color: var(--faint);
}
.chip.active {
background: var(--text);
border-color: var(--text);
color: var(--bg);
font-weight: 500;
}
.alert {
padding: 10px 14px;
border: 1px solid color-mix(in srgb, var(--danger) 45%, var(--border));
border-radius: var(--radius);
background: color-mix(in srgb, var(--danger) 10%, transparent);
font-size: var(--fs-md);
}
.ok {
padding: 10px 14px;
border: 1px solid color-mix(in srgb, var(--success) 40%, var(--border));
border-radius: var(--radius);
background: color-mix(in srgb, var(--success) 9%, transparent);
font-size: var(--fs-md);
}
.muted {
color: var(--muted);
}
.faint {
color: var(--faint);
}
.mono {
font-family: var(--mono);
font-size: var(--fs-sm);
}
/* ── Cover ──────────────────────────────────────────────────────────── */
.cover {
position: relative;
aspect-ratio: 2 / 3;
border-radius: var(--radius-cover);
background: var(--surface-2);
overflow: hidden;
box-shadow: var(--shadow-cover);
}
.cover.wide {
aspect-ratio: 1 / 1;
}
.cover img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.cover .placeholder {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-size: 2.2rem;
font-weight: 600;
color: var(--faint);
background: linear-gradient(150deg, var(--surface-2), var(--surface-3));
}
.cover img + .placeholder {
display: none;
}
/* ── Empty ──────────────────────────────────────────────────────────── */
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-7) var(--space-4);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
text-align: center;
color: var(--muted);
background: var(--surface);
}
.empty h3 {
color: var(--text);
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid var(--border-strong);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.skeleton {
background: linear-gradient(90deg, var(--surface-2) 25%, var(--surface-3) 37%, var(--surface-2) 63%);
background-size: 400% 100%;
animation: shimmer 1.4s ease infinite;
border-radius: var(--radius-cover);
}
@keyframes shimmer {
0% {
background-position: 100% 0;
}
100% {
background-position: -100% 0;
}
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-thumb {
background: var(--border-strong);
border-radius: 999px;
border: 3px solid var(--bg);
}