Files
bookhoard/web/static/input.css
T
john-okeefe e500039d1b feat(reader): webtoon reading mode + brightness/contrast/night filters
Phase 4 of the reader redesign (foliate-js ea268df):

- Webtoon mode for comics: continuous vertical scroll of all pages
  (900px centered column on wide screens), lazy-loaded with a 150%
  IntersectionObserver margin, far pages unloaded to bound memory
  with stable aspect-ratio placeholders so the scrollbar never jumps.
  Chosen per book (Paged | Webtoon segmented control in Settings →
  Layout & Display; stored in localStorage per media item since a
  webtoon title and a paged manga volume want different flows).
  Toggling reloads the reader — the renderer is chosen at open time —
  and progress restores from the saved page. Relocate events flow
  through the same pipeline, so the slider, progress saving, back
  stack, tap zones, and edge zones all work unchanged. Zoom/fit/
  magnifier/spread controls hide in webtoon (natural-width scroll).
- Display filters for fixed-layout: brightness (30-130%) and
  contrast (70-130%) sliders with live preview, plus Night Mode
  (invert) — also a quick row in the ⋯ tools menu. One --fx-filter
  CSS var drives everything: ::part(filter) on foliate-view iframes
  (forwarded via the new exportparts attribute) and the webtoon
  page images alike. Persisted as fx_brightness/fx_contrast/fx_invert
  (types + defaults both sides); Restore Defaults resets them.
2026-08-18 08:25:00 -04:00

1437 lines
37 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;
}
/* Selection popover & annotations drawer widgets */
.reader-popover {
position: fixed;
transform: translate(-50%, calc(-100% - 10px));
background-color: color-mix(in srgb, var(--bg-primary) 85%, transparent);
-webkit-backdrop-filter: blur(16px) saturate(1.3);
backdrop-filter: blur(16px) saturate(1.3);
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
border-radius: 0.75rem;
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
padding: 0.5rem 0.625rem;
z-index: 60;
}
.color-dot {
width: 1.375rem;
height: 1.375rem;
border-radius: 9999px;
border: 2px solid rgba(0, 0, 0, 0.25);
transition: transform 0.12s ease;
}
.color-dot:hover {
transform: scale(1.15);
}
.color-dot.selected {
border-color: #ffffff;
box-shadow: 0 0 0 2px #3b82f6;
}
.reader-popover-btn {
padding: 0.3rem;
border-radius: 0.375rem;
color: inherit;
}
.reader-popover-btn:hover {
background-color: color-mix(in srgb, currentColor 13%, transparent);
}
.reader-popover-btn.danger:hover {
background-color: rgba(153, 27, 27, 0.6);
}
.reader-note-input {
width: 100%;
resize: vertical;
padding: 0.5rem;
border-radius: 0.5rem;
font-size: 0.875rem;
color: inherit;
background-color: color-mix(in srgb, var(--bg-primary) 55%, transparent);
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}
.reader-note-input:focus {
outline: 2px solid #3b82f6;
outline-offset: 1px;
}
.reader-tabs {
display: flex;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.reader-tabs button {
flex: 1;
padding: 0.5rem 0.25rem;
font-size: 0.8125rem;
color: var(--text-secondary);
border-bottom: 2px solid transparent;
}
.reader-tabs button.active {
color: var(--text-primary);
border-bottom-color: #3b82f6;
}
.reader-tab-count {
display: inline-block;
min-width: 1.25rem;
margin-left: 0.25rem;
padding: 0 0.25rem;
border-radius: 9999px;
font-size: 0.6875rem;
line-height: 1.1rem;
background-color: color-mix(in srgb, currentColor 13%, transparent);
}
.reader-hl-row {
display: flex;
align-items: center;
gap: 0.25rem;
}
/* Fixed-layout toolbar responsiveness is handled with Tailwind responsive
utilities in reader.templ (hidden md:flex for the full toolbar,
flex md:hidden for the compact row) — custom layer rules here would
lose the cascade to the flex utility anyway. */
.reader-tools-popover {
position: absolute;
bottom: 100%;
right: 0.5rem;
margin-bottom: 0.5rem;
min-width: 16rem;
max-width: calc(100vw - 1.5rem);
max-height: min(26rem, 65vh);
overflow-y: auto;
background-color: color-mix(in srgb, var(--bg-primary) 88%, transparent);
-webkit-backdrop-filter: blur(18px) saturate(1.3);
backdrop-filter: blur(18px) saturate(1.3);
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
border-radius: 0.75rem;
box-shadow: 0 -6px 28px rgba(0, 0, 0, 0.35);
padding: 0.25rem;
}
.reader-tools-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
border-radius: 0.5rem;
}
.reader-tools-row + .reader-tools-row {
border-top: 1px solid
color-mix(in srgb, var(--border) 45%, transparent);
}
.reader-tools-row:hover {
background-color: color-mix(in srgb, currentColor 6%, transparent);
}
/* Fixed-layout display filters: one var drives the iframe ::part(filter)
(comics/PDFs via foliate-view exportparts) and the webtoon page images. */
#reader-view::part(filter) {
filter: var(--fx-filter, none);
}
/* Page thumbnails grid (contents drawer, fixed-layout) */
.reader-thumb-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.reader-thumb {
position: relative;
border-radius: 0.375rem;
overflow: hidden;
border: 2px solid transparent;
padding: 0;
background-color: color-mix(in srgb, currentColor 6%, transparent);
}
.reader-thumb:hover {
border-color: color-mix(in srgb, currentColor 30%, transparent);
}
.reader-thumb.active {
border-color: #3b82f6;
}
.reader-thumb-img {
aspect-ratio: 3 / 4;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.reader-thumb-img canvas,
.reader-thumb-img img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.reader-thumb-num {
position: absolute;
bottom: 0.2rem;
right: 0.35rem;
font-size: 0.65rem;
line-height: 1;
padding: 0.15rem 0.3rem;
border-radius: 0.25rem;
background-color: rgba(0, 0, 0, 0.55);
color: #ffffff;
}
/* Desktop edge page-turn zones: only on hover-capable fine-pointer
devices (touch uses tap zones instead). Arrow + subtle edge gradient
appear on hover. */
.reader-edge-zone {
position: absolute;
top: 0;
bottom: 0;
width: 8%;
max-width: 72px;
min-width: 44px;
z-index: 10;
display: none;
align-items: center;
cursor: pointer;
}
.reader-edge-zone.left {
left: 0;
justify-content: flex-start;
padding-left: 0.75rem;
}
.reader-edge-zone.right {
right: 0;
justify-content: flex-end;
padding-right: 0.75rem;
}
.reader-edge-zone::before {
content: "";
position: absolute;
inset: 0;
opacity: 0;
transition: opacity 0.15s ease;
}
.reader-edge-zone.left::before {
background: linear-gradient(to right, rgba(255, 255, 255, 0.08), transparent);
}
.reader-edge-zone.right::before {
background: linear-gradient(to left, rgba(255, 255, 255, 0.08), transparent);
}
.reader-edge-zone .reader-icon {
color: #ffffff;
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.6));
opacity: 0;
transition: opacity 0.15s ease;
}
.reader-edge-zone:hover::before {
opacity: 1;
}
.reader-edge-zone:hover .reader-icon {
opacity: 0.85;
}
@media (hover: hover) and (pointer: fine) {
.reader-edge-zone {
display: flex;
}
}
/* Shortcuts help modal */
.reader-help-backdrop {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 70;
}
.reader-help {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(34rem, calc(100vw - 2rem));
max-height: min(38rem, calc(100vh - 4rem));
display: flex;
flex-direction: column;
background-color: color-mix(in srgb, var(--bg-primary) 90%, transparent);
-webkit-backdrop-filter: blur(20px) saturate(1.3);
backdrop-filter: blur(20px) saturate(1.3);
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
border-radius: 0.875rem;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
z-index: 71;
}
.reader-help-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.875rem 1.25rem;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.reader-help-body {
overflow-y: auto;
padding: 1rem 1.25rem;
}
.reader-help-section {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-secondary);
margin: 1.1rem 0 0.4rem;
}
.reader-help-body .reader-help-section:first-child {
margin-top: 0;
}
.help-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
padding: 0.3rem 0;
font-size: 0.85rem;
}
.help-row > span:first-child {
color: var(--text-primary);
}
.help-keys {
text-align: right;
color: var(--text-secondary);
font-size: 0.8rem;
white-space: nowrap;
}
.help-note {
color: var(--text-secondary);
font-size: 0.75rem;
}
.kbd {
display: inline-block;
min-width: 1.4rem;
text-align: center;
padding: 0.05rem 0.4rem;
margin: 0 0.1rem;
border-radius: 0.3rem;
border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
border-bottom-width: 2px;
background-color: color-mix(in srgb, currentColor 8%, transparent);
font-family: ui-monospace, monospace;
font-size: 0.72rem;
line-height: 1.2rem;
}
/* In-book search */
.reader-search-bar {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.625rem 1rem;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.reader-search-status {
padding: 0.375rem 1rem;
font-size: 0.75rem;
color: var(--text-secondary);
border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
flex-shrink: 0;
min-height: 1.75rem;
}
.search-result {
display: block;
padding: 0.375rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.8125rem;
line-height: 1.35;
}
.search-result:hover {
background-color: color-mix(in srgb, currentColor 9%, transparent);
}
.search-result mark {
background-color: rgba(255, 213, 79, 0.4);
color: inherit;
border-radius: 2px;
padding: 0 1px;
}
/* ---------- 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%);
}
}
}