Files
bookhoard/web/static/input.css
T
john-okeefe 24ea9d8a38 feat(reader): touch & mobile — tap zones, gesture engine, mobile sheets
Phase 2 of the reader redesign:

- Fixed-layout touch engine (foliate-js e9e61d8): pinch-zoom around
  the midpoint, two-finger pan, single-finger pan while zoomed,
  horizontal swipe page-turn at fit (RTL-aware via next()/prev()),
  and double-tap to zoom 2.5x / reset. Touch events forwarded from
  page iframes with converted coordinates; preventDefault only when
  the engine consumes the gesture, so PDF text selection and native
  taps stay intact. touch-action: none on the host and in comic/pdf
  page documents keeps the browser from fighting the engine.
- Tap zones (Kindle-style) for touch devices: tap the outer margins
  to page, center to toggle chrome. Size configurable (10-50%) via
  the revived tap_zone_size setting; toggle via new tap_zones_enabled
  (Behavior section of the settings drawer). Pointer-based + passive
  so drags/swipes/selection never trigger; attached both to the
  viewport and inside every page document (iframe events don't
  bubble); debounced 280ms so double-tap zoom doesn't also page; no
  zone actions while a fixed-layout page is zoomed.
- Drawers become full-width sheets on screens <= 640px.
2026-08-14 16:00:19 -04:00

1092 lines
28 KiB
CSS

/* Tailwind CSS directives - required for framework */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
/*
* Default :root mirrors the Tokyo Night theme so the palette is correct
* on first paint, before the theme class is applied to <body> (kills the
* theme-flash on load).
*/
:root {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #c0caf5;
--text-secondary: #9aa5ce;
--accent: #7aa2f7;
--border: #2a2e42;
/* status */
--status-success: #9ece6a;
--status-warning: #e0af68;
--status-danger: #f7768e;
--status-info: #7aa2f7;
}
.theme-tokyo-night {
--bg-primary: #1a1b26;
--bg-secondary: #16161e;
--text-primary: #c0caf5;
--text-secondary: #9aa5ce;
--accent: #7aa2f7;
--border: #2a2e42;
}
.theme-dracula {
--bg-primary: #282a36;
--bg-secondary: #21222c;
--text-primary: #f8f8f2;
--text-secondary: #bfbfbf;
--accent: #bd93f9;
--border: #3a3d4d;
}
.theme-nord {
--bg-primary: #2e3440;
--bg-secondary: #3b4252;
--text-primary: #eceff4;
--text-secondary: #b7c0d2;
--accent: #88c0d0;
--border: #46577a;
}
.theme-solarized-dark {
--bg-primary: #002b36;
--bg-secondary: #073642;
--text-primary: #c5d3d3;
--text-secondary: #93a1a1;
--accent: #2aa198;
--border: #1d4b58;
}
.theme-monokai {
--bg-primary: #272822;
--bg-secondary: #1e1f1c;
--text-primary: #f8f8f2;
--text-secondary: #c8c8b8;
--accent: #a6e22e;
--border: #3e3d32;
}
.theme-one-dark-pro {
--bg-primary: #282c34;
--bg-secondary: #21252b;
--text-primary: #d7dbe0;
--text-secondary: #9da5b4;
--accent: #61dafb;
--border: #3a3f4b;
}
.theme-material-dark {
--bg-primary: #263238;
--bg-secondary: #1e282d;
--text-primary: #eeffff;
--text-secondary: #b0bec5;
--accent: #80cbc4;
--border: #37474f;
}
.theme-catppuccin-mocha {
--bg-primary: #1e1e2e;
--bg-secondary: #181825;
--text-primary: #cdd6f4;
--text-secondary: #a6adc8;
--accent: #f38ba8;
--border: #313244;
}
.theme-catppuccin-macchiato {
--bg-primary: #24273a;
--bg-secondary: #1e2030;
--text-primary: #cad3f5;
--text-secondary: #a5adcb;
--accent: #c6a0f6;
--border: #363a4f;
}
.theme-catppuccin-frappe {
--bg-primary: #303446;
--bg-secondary: #292c3c;
--text-primary: #c6d0f5;
--text-secondary: #a5adce;
--accent: #ca9ee6;
--border: #414559;
}
.theme-catppuccin-latte {
--bg-primary: #eff1f5;
--bg-secondary: #ffffff;
--text-primary: #4c4f69;
--text-secondary: #6c6f85;
--accent: #d20f39;
--border: #dcdfe6;
}
/*
* Derived tokens — computed once on <body> from the active theme vars so
* they adapt automatically to every theme without 11x repetition.
*/
body {
background-color: var(--bg-primary);
color: var(--text-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* hover/overlay surfaces */
--surface-hover: color-mix(in srgb, var(--text-primary) 9%, transparent);
--surface-overlay: rgba(0, 0, 0, 0.55);
/* stronger hairline */
--border-strong: color-mix(in srgb, var(--text-primary) 24%, var(--border));
/* soft accent tint (chips, active nav, subtle highlights) */
--accent-muted: color-mix(in srgb, var(--accent) 14%, var(--bg-secondary));
/* elevation */
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.10), 0 1px 3px rgba(0, 0, 0, 0.08);
--shadow-card-hover: 0 6px 16px rgba(0, 0, 0, 0.22), 0 2px 6px rgba(0, 0, 0, 0.12);
--shadow-pop: 0 12px 40px rgba(0, 0, 0, 0.34), 0 4px 12px rgba(0, 0, 0, 0.20);
--shadow-bar: 0 1px 0 rgba(0, 0, 0, 0.18), 0 4px 16px rgba(0, 0, 0, 0.16);
}
/* Slightly softer shadows on the lone light theme */
.theme-catppuccin-latte {
--shadow-card: 0 1px 2px rgba(76, 79, 105, 0.08), 0 1px 3px rgba(76, 79, 105, 0.06);
--shadow-card-hover: 0 6px 16px rgba(76, 79, 105, 0.16), 0 2px 6px rgba(76, 79, 105, 0.08);
--shadow-pop: 0 12px 40px rgba(76, 79, 105, 0.22), 0 4px 12px rgba(76, 79, 105, 0.12);
--shadow-bar: 0 1px 0 rgba(76, 79, 105, 0.08), 0 4px 16px rgba(76, 79, 105, 0.10);
}
/* App-wide keyboard focus affordance (accessibility) */
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* Hide Alpine-controlled elements until Alpine initialises */
[x-cloak] {
display: none !important;
}
/*
* App shell — a fixed left sidebar with a content area to its right.
* The sidebar is off-canvas on mobile (toggled via the `is-open` class,
* which Alpine flips through `mobileMenuOpen`) and pinned on >= lg.
*/
:root {
--sidebar-w: 16rem;
--topbar-h: 4rem;
}
.app-sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: var(--sidebar-w);
z-index: 60;
background-color: var(--bg-secondary);
border-right: 1px solid color-mix(in srgb, var(--text-primary) 7%, transparent);
display: flex;
flex-direction: column;
overflow-y: auto;
transform: translateX(-100%);
transition: transform 0.25s ease;
}
.app-sidebar.is-open {
transform: translateX(0);
}
.app-sidebar-backdrop {
position: fixed;
inset: 0;
z-index: 55;
background: var(--surface-overlay);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
.app-sidebar-backdrop.is-open {
opacity: 1;
pointer-events: auto;
}
/* Grouped panels for the collapsible footer menus (account, appearance).
Each menu lives in its own subtly lifted, bordered box so that expanded
submenus stay visually contained and one menu never blends into another. */
.sidebar-panel {
background-color: color-mix(in srgb, var(--text-primary) 4%, var(--bg-secondary));
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 0.375rem;
}
/* Desktop: pin the sidebar, offset body content, drop the backdrop. */
@media (min-width: 1024px) {
body {
padding-left: var(--sidebar-w);
}
.app-sidebar {
transform: none;
}
.app-sidebar-backdrop {
display: none !important;
}
.app-mobile-only {
display: none !important;
}
}
@media (max-width: 1023.98px) {
.app-desktop-only {
display: none !important;
}
}
/* Slim fixed topbar that spans the content area (right of the sidebar). */
.app-topbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 40;
height: var(--topbar-h);
background-color: color-mix(in srgb, var(--bg-secondary) 82%, transparent);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid color-mix(in srgb, var(--text-primary) 6%, transparent);
}
/*
* Content offsets — applied only on pages whose Header renders the
* fixed sidebar (reader.templ / error.templ have no sidebar, so they are
* left untouched).
*/
body:has(.app-sidebar) {
padding-top: var(--topbar-h);
}
@media (min-width: 1024px) {
body:has(.app-sidebar) {
padding-left: var(--sidebar-w);
}
/* On desktop the topbar starts right of the pinned sidebar. */
.app-topbar {
left: var(--sidebar-w);
}
}
/* A secondary sticky bar (e.g. library switcher) parks under the topbar. */
.app-subbar {
position: sticky;
top: var(--topbar-h);
z-index: 30;
}
/* Smooth, theme-aware scrollbars */
* {
scrollbar-color: color-mix(in srgb, var(--text-primary) 22%, transparent) transparent;
}
}
@layer components {
/* ---------- Surfaces ---------- */
.card {
background-color: var(--bg-secondary);
border: 1px solid color-mix(in srgb, var(--text-primary) 6%, transparent);
border-radius: 1rem;
box-shadow: var(--shadow-card);
}
/* ---------- Buttons ---------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.625rem;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.25rem;
transition: background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease,
transform 0.15s ease, border-color 0.15s ease;
cursor: pointer;
white-space: nowrap;
}
.btn:active {
transform: translateY(1px);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background-color: var(--accent);
color: var(--bg-primary);
box-shadow: var(--shadow-card);
}
.btn-primary:hover {
background-color: color-mix(in srgb, var(--accent) 88%, white);
box-shadow: var(--shadow-card-hover);
}
.btn-secondary {
background-color: transparent;
color: var(--text-primary);
border: 1px solid var(--border-strong);
}
.btn-secondary:hover {
background-color: var(--surface-hover);
border-color: var(--accent);
color: var(--text-primary);
}
.btn-ghost {
background-color: transparent;
color: var(--text-secondary);
}
.btn-ghost:hover {
background-color: var(--surface-hover);
color: var(--text-primary);
}
.btn-danger {
background-color: var(--status-danger);
color: #fff;
}
.btn-danger:hover {
background-color: color-mix(in srgb, var(--status-danger) 88%, white);
}
/* ---------- Icon button (square, subtle) ---------- */
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.625rem;
color: var(--text-secondary);
transition: background-color 0.15s ease, color 0.15s ease;
cursor: pointer;
}
.icon-btn:hover {
background-color: var(--surface-hover);
color: var(--text-primary);
}
/* ---------- Inputs ---------- */
.input {
width: 100%;
background-color: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: 0.625rem;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.input::placeholder {
color: color-mix(in srgb, var(--text-secondary) 75%, transparent);
}
.input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);
outline: none;
}
/* ---------- Chips & badges ---------- */
.chip {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
background-color: var(--accent-muted);
color: var(--text-primary);
}
.badge {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.01em;
}
/* ---------- Book card (the atomic unit) ---------- */
.book-card {
position: relative;
background-color: var(--bg-secondary);
border: 1px solid color-mix(in srgb, var(--text-primary) 6%, transparent);
border-radius: 0.75rem;
overflow: hidden;
box-shadow: var(--shadow-card);
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.book-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-card-hover);
border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
}
/* Cinematic cover overlay: a centered quick-action that fades in on hover. */
.book-card-cover {
position: relative;
}
.book-card-cover::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.55), transparent 55%);
opacity: 0;
transition: opacity 0.18s ease;
pointer-events: none;
}
.book-card:hover .book-card-cover::after {
opacity: 1;
}
.book-card-action {
position: absolute;
top: 0;
left: 0;
right: 0;
aspect-ratio: 2 / 3;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
z-index: 2;
transition: opacity 0.18s ease;
}
.book-card:hover .book-card-action,
.book-card:focus-within .book-card-action {
opacity: 1;
}
.book-card-action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.75rem;
height: 2.75rem;
border-radius: 9999px;
background-color: color-mix(in srgb, var(--accent) 92%, white);
color: #fff;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
pointer-events: auto;
text-decoration: none;
}
.book-card-meta {
background-color: var(--bg-secondary);
padding: 0.5rem 0.625rem;
}
/* On touch devices there is no hover, so the quick-action stays visible and
only the circular button itself is clickable (the container passes clicks
through to the detail link). */
@media (hover: none) {
.book-card-action {
opacity: 1;
}
}
/* ---------- Bold accents ---------- */
/* Signature gradient surface for heroes / primary panels. */
.brand-gradient {
background-image: linear-gradient(
135deg,
color-mix(in srgb, var(--accent) 28%, var(--bg-secondary)),
var(--bg-secondary) 62%
);
}
/* Blurred cover backdrop used behind the book-detail hero. */
.hero-backdrop {
position: absolute;
inset: 0;
z-index: 0;
background-size: cover;
background-position: center;
filter: blur(28px) saturate(1.4);
transform: scale(1.15);
opacity: 0.5;
}
.hero-backdrop::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
color-mix(in srgb, var(--bg-primary) 35%, transparent),
var(--bg-primary) 92%
);
}
/* Stat tile for analytics / dashboard summaries. */
.stat-card {
background-color: var(--bg-secondary);
border: 1px solid color-mix(in srgb, var(--text-primary) 6%, transparent);
border-radius: 1rem;
padding: 1.25rem;
box-shadow: var(--shadow-card);
}
/* ---------- TriState Button (has_cover filter) ---------- */
.tristate-btn {
background-color: var(--bg-primary);
border: 1px solid var(--border);
}
.tristate-btn.state-null {
color: var(--text-secondary);
}
.tristate-btn.state-true {
background-color: var(--accent-muted);
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
color: var(--accent);
}
.tristate-btn.state-false {
background-color: var(--surface-hover);
color: var(--text-secondary);
}
.loading-spinner {
display: inline-block;
width: 40px;
height: 40px;
border: 3px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* ---------- Status / priority semantics ---------- */
.status-pending {
background-color: color-mix(in srgb, var(--status-warning) 20%, transparent);
color: var(--status-warning);
}
.status-processing {
background-color: color-mix(in srgb, var(--status-info) 20%, transparent);
color: var(--status-info);
}
.status-completed {
background-color: color-mix(in srgb, var(--status-success) 20%, transparent);
color: var(--status-success);
}
.status-failed {
background-color: color-mix(in srgb, var(--status-danger) 20%, transparent);
color: var(--status-danger);
}
.priority-1 {
background-color: color-mix(in srgb, var(--status-danger) 22%, transparent);
color: var(--status-danger);
}
.priority-2 {
background-color: color-mix(in srgb, #f97316 22%, transparent);
color: #fb923c;
}
.priority-3 {
background-color: color-mix(in srgb, var(--status-warning) 22%, transparent);
color: var(--status-warning);
}
.priority-5 {
background-color: color-mix(in srgb, var(--status-success) 22%, transparent);
color: var(--status-success);
}
.priority-7 {
background-color: var(--surface-hover);
color: var(--text-secondary);
}
.priority-10 {
background-color: var(--surface-hover);
color: var(--text-secondary);
}
/* ---------- Docs / highlight.js overrides ---------- */
.prose pre code.hljs {
background-color: transparent !important;
}
.prose code.hljs {
background-color: var(--bg-primary) !important;
padding: 0.2em 0.4em;
}
.prose pre {
background-color: var(--bg-secondary) !important;
}
.prose .hljs {
background-color: transparent !important;
color: var(--text-primary) !important;
}
/* ---------- Theme / wood picker active states ---------- */
.bg-theme-active,
.bg-wood-active {
background-color: var(--bg-primary) !important;
box-shadow: inset 0 0 0 2px var(--accent);
}
.bg-theme-inactive,
.bg-wood-inactive {
background-color: var(--bg-secondary) !important;
}
/* ---------- Wood paneling (optional bookshelf background) ---------- */
.bg-wood-dark,
.bg-wood-light,
.bg-wood-mahogany {
background-attachment: fixed;
background-position: center;
background-size: cover;
}
.bg-wood-dark {
background-image: url(/static/textures/wood-dark.png);
}
.bg-wood-light {
background-image: url(/static/textures/wood-light.png);
}
.bg-wood-mahogany {
background-image: url(/static/textures/wood-mahogany.png);
}
body.bg-wood-dark,
body.bg-wood-light,
body.bg-wood-mahogany,
#collections-container.bg-wood-dark,
#collections-container.bg-wood-light,
#collections-container.bg-wood-mahogany {
background-attachment: fixed;
background-position: center;
background-size: cover;
}
/* Smart text colors for wood paneling backgrounds */
#collections-container[data-wood="wood-light"] {
--wood-text-primary: #1a1a1a;
--wood-text-secondary: #4a4a4a;
--wood-border: #8b5a2b;
}
#collections-container[data-wood="wood-dark"] {
--wood-text-primary: #f5f5f5;
--wood-text-secondary: #c0c0c0;
--wood-border: #7a5228;
}
#collections-container[data-wood="wood-mahogany"] {
--wood-text-primary: #f5f5f5;
--wood-text-secondary: #c0c0c0;
--wood-border: #8b3a3a;
}
#collections-container[data-wood] h1,
#collections-container[data-wood] h2,
#collections-container[data-wood] h3,
#collections-container[data-wood] p,
#collections-container[data-wood] span,
#collections-container[data-wood] a,
#collections-container[data-wood] button {
color: var(--wood-text-primary) !important;
}
#collections-container[data-wood] .text-secondary {
color: var(--wood-text-secondary) !important;
}
#collections-container[data-wood="wood-light"] a {
color: #0066cc !important;
}
#collections-container[data-wood="wood-dark"] a,
#collections-container[data-wood="wood-mahogany"] a {
color: #66ccff !important;
}
#collections-container[data-wood="wood-light"] .carousel-nav-left {
background: linear-gradient(to right, rgba(0, 0, 0, 0.6), transparent);
}
#collections-container[data-wood="wood-light"] .carousel-nav-right {
background: linear-gradient(to left, rgba(0, 0, 0, 0.6), transparent);
}
#collections-container[data-wood="wood-dark"] .carousel-nav-left,
#collections-container[data-wood="wood-mahogany"] .carousel-nav-left {
background: linear-gradient(to right, rgba(0, 0, 0, 0.3), transparent);
}
#collections-container[data-wood="wood-dark"] .carousel-nav-right,
#collections-container[data-wood="wood-mahogany"] .carousel-nav-right {
background: linear-gradient(to left, rgba(0, 0, 0, 0.3), transparent);
}
#collections-container[data-wood] .book-card {
border: 1px solid var(--wood-border);
}
/* ---------- Reader chrome & drawers ---------- */
.reader-icon {
display: block;
fill: none;
stroke: currentColor;
stroke-width: 2px;
stroke-linecap: round;
stroke-linejoin: round;
}
/* Glass chrome: translucent theme-tinted bars over the edge-to-edge
reading surface. Blur + saturate the content behind, hairline border,
soft directional shadow. */
.reader-glass {
background-color: color-mix(in srgb, var(--bg-primary) 70%, transparent);
-webkit-backdrop-filter: blur(18px) saturate(1.4);
backdrop-filter: blur(18px) saturate(1.4);
border-color: color-mix(in srgb, var(--border) 60%, transparent);
}
#reader-topbar.reader-glass {
border-bottom: 1px solid
color-mix(in srgb, var(--border) 60%, transparent);
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.16);
}
#reader-bottombar.reader-glass {
border-top: 1px solid
color-mix(in srgb, var(--border) 60%, transparent);
box-shadow: 0 -4px 18px rgba(0, 0, 0, 0.16);
}
/* Bars slide away when the chrome hides (opacity handled on #reader-chrome) */
#reader-topbar,
#reader-bottombar {
transition:
transform 0.3s ease,
opacity 0.3s ease;
}
#reader-chrome.chrome-hidden #reader-topbar {
transform: translateY(-101%);
}
#reader-chrome.chrome-hidden #reader-bottombar {
transform: translateY(101%);
}
/* Theme-aware translucent hover pills + focus rings for bar controls */
#reader-topbar button:hover,
#reader-bottombar button:hover,
#reader-topbar a:hover {
background-color: color-mix(in srgb, currentColor 13%, transparent);
}
#reader-topbar button:focus-visible,
#reader-bottombar button:focus-visible,
#reader-topbar a:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.reader-sep {
background-color: color-mix(in srgb, var(--border) 80%, transparent);
}
.reader-select {
background-color: color-mix(in srgb, var(--bg-primary) 55%, transparent);
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
color: inherit;
}
.reader-select:hover {
border-color: #3b82f6;
}
/* Progress slider: thin rounded track + floating white thumb */
#reader-bottombar input[type="range"] {
-webkit-appearance: none;
appearance: none;
height: 18px;
background: transparent;
cursor: pointer;
accent-color: #3b82f6;
}
#reader-bottombar input[type="range"]::-webkit-slider-runnable-track {
height: 4px;
border-radius: 9999px;
background: color-mix(in srgb, currentColor 22%, transparent);
}
#reader-bottombar input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
margin-top: -5px;
width: 14px;
height: 14px;
border: none;
border-radius: 50%;
background: #ffffff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
transition: transform 0.12s ease;
}
#reader-bottombar input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.15);
}
#reader-bottombar input[type="range"]::-moz-range-track {
height: 4px;
border-radius: 9999px;
background: color-mix(in srgb, currentColor 22%, transparent);
}
#reader-bottombar input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
border: none;
border-radius: 50%;
background: #ffffff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
}
.drawer-scrim {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.45);
z-index: 45;
}
.reader-drawer {
position: fixed;
top: 0;
bottom: 0;
width: 340px;
max-width: calc(100vw - 2rem);
background-color: var(--bg-secondary);
z-index: 50;
display: flex;
flex-direction: column;
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
.reader-drawer.left {
left: 0;
border-right: 1px solid var(--border);
}
.reader-drawer.right {
right: 0;
border-left: 1px solid var(--border);
}
/* Mobile: drawers become full-width sheets */
@media (max-width: 640px) {
.reader-drawer {
width: 100%;
max-width: 100%;
}
}
.reader-drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.reader-drawer-body {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.reader-seg {
display: inline-flex;
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
background-color: color-mix(in srgb, var(--bg-primary) 50%, transparent);
border-radius: 0.5rem;
overflow: hidden;
}
.reader-seg button {
padding: 0.25rem 0.6rem;
font-size: 0.75rem;
line-height: 1.25rem;
}
.reader-seg button.active {
background-color: #2563eb;
color: #ffffff;
}
.theme-swatch {
height: 2.25rem;
border-radius: 0.5rem;
border: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
}
.theme-swatch:hover {
border-color: #3b82f6;
}
/* ---------- Series stacked covers ---------- */
.stacked-covers {
position: relative;
width: 100%;
aspect-ratio: 2 / 3;
overflow: hidden;
}
.stacked-covers .cover-img {
position: absolute;
width: 70%;
height: 90%;
object-fit: cover;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
.stacked-covers.cover-count-1 .cover-0 {
top: 5%;
left: 15%;
}
.stacked-covers.cover-count-2 .cover-0 {
top: 8%;
left: 5%;
transform: rotate(-6deg);
z-index: 1;
}
.stacked-covers.cover-count-2 .cover-1 {
top: 2%;
left: 25%;
transform: rotate(3deg);
z-index: 2;
}
.stacked-covers.cover-count-3 .cover-0 {
top: 12%;
left: 2%;
transform: rotate(-8deg);
z-index: 1;
}
.stacked-covers.cover-count-3 .cover-1 {
top: 6%;
left: 15%;
transform: rotate(-2deg);
z-index: 2;
}
.stacked-covers.cover-count-3 .cover-2 {
top: 0;
left: 30%;
transform: rotate(4deg);
z-index: 3;
}
.stacked-covers.cover-count-4 .cover-0 {
top: 14%;
left: 0;
width: 65%;
height: 85%;
transform: rotate(-10deg);
z-index: 1;
}
.stacked-covers.cover-count-4 .cover-1 {
top: 10%;
left: 12%;
width: 65%;
height: 85%;
transform: rotate(-4deg);
z-index: 2;
}
.stacked-covers.cover-count-4 .cover-2 {
top: 5%;
left: 22%;
width: 65%;
height: 85%;
transform: rotate(2deg);
z-index: 3;
}
.stacked-covers.cover-count-4 .cover-3 {
top: 0;
left: 32%;
width: 65%;
height: 85%;
transform: rotate(6deg);
z-index: 4;
}
.stacked-covers.cover-count-5 .cover-0,
.stacked-covers.cover-count-6 .cover-0,
.stacked-covers.cover-count-7 .cover-0 {
top: 16%;
left: -2%;
width: 62%;
height: 82%;
transform: rotate(-12deg);
z-index: 1;
}
.stacked-covers.cover-count-5 .cover-1,
.stacked-covers.cover-count-6 .cover-1,
.stacked-covers.cover-count-7 .cover-1 {
top: 12%;
left: 8%;
width: 62%;
height: 82%;
transform: rotate(-6deg);
z-index: 2;
}
.stacked-covers.cover-count-5 .cover-2,
.stacked-covers.cover-count-6 .cover-2,
.stacked-covers.cover-count-7 .cover-2 {
top: 8%;
left: 17%;
width: 62%;
height: 82%;
transform: rotate(-1deg);
z-index: 3;
}
.stacked-covers.cover-count-5 .cover-3,
.stacked-covers.cover-count-6 .cover-3,
.stacked-covers.cover-count-7 .cover-3 {
top: 4%;
left: 26%;
width: 62%;
height: 82%;
transform: rotate(4deg);
z-index: 4;
}
.stacked-covers.cover-count-5 .cover-4,
.stacked-covers.cover-count-6 .cover-4,
.stacked-covers.cover-count-7 .cover-4 {
top: 0;
left: 35%;
width: 62%;
height: 82%;
transform: rotate(9deg);
z-index: 5;
}
.stacked-covers.cover-count-6 .cover-5,
.stacked-covers.cover-count-7 .cover-5 {
top: -2%;
left: 30%;
width: 60%;
height: 80%;
transform: rotate(13deg);
z-index: 6;
}
.stacked-covers.cover-count-7 .cover-6 {
top: -4%;
left: 25%;
width: 58%;
height: 78%;
transform: rotate(16deg);
z-index: 7;
}
.series-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.series-card:hover {
transform: translateY(-4px);
}
/* ---------- Skeleton loading shimmer ---------- */
.skeleton {
position: relative;
overflow: hidden;
background-color: var(--surface-hover);
border-radius: 0.5rem;
}
.skeleton::after {
content: "";
position: absolute;
inset: 0;
transform: translateX(-100%);
background: linear-gradient(
90deg,
transparent,
color-mix(in srgb, var(--text-primary) 8%, transparent),
transparent
);
animation: skeleton-shimmer 1.4s infinite;
}
@keyframes skeleton-shimmer {
100% {
transform: translateX(100%);
}
}
}