Files
bookhoard/web/static/input.css
T
John O'Keefe 73ae1789c5 feat(reader): fullscreen mode; revert stacked volume; fix manga badges + ghost sidebar
Reader fullscreen: the topbar gains a fullscreen toggle (icon state + f
shortcut + help row; f is guarded so Ctrl/Cmd+F stays find-in-page).
Enter requests the Fullscreen API with fallbacks (desktop/Android get a
true full display; iOS Safari has no page-fullscreen API and falls back
to the soft visual mode only). Margins: the foliate paginator's closed
shadow root only yields through its observed 'margin' attribute, so
fullscreen fits a comfortable ratio (6% of the short side, floored at
48px) and floors it by the measured env insets — Android reports true
cutout insets in fullscreen, so portrait phones keep text clear of the
camera band; iOS letterboxes are already env-padded chrome. Fixed-layout
renderers skip it (fxl/webtoon pack pages edge-to-edge by design).
Exits the user makes outside the toggle (Esc, browser UI) sync via
fullscreenchange(+webkit) and restore the ordinary 48px margin; a
position-keeping re-render follows the ResizeObserver re-entry path.
Verified on-device: enter -> true fullscreen + icon swap + camera band
clear, exit -> chrome back, margins restored, position kept.

Volume: revert the stacked panel — the popup returns to the liked shape
(mute button left, horizontal dragger right, dialogue above the strip);
one-line flex restore of the pre-89665fb layout.

Manga badges: MediaBadgeKind makes the library type authoritative for
the comic/manga split — manga volumes ship as fixed_layout scans as
often as comic_archive (and manga_type is unreliable across sources:
unknown/NULL on real rows), so the old comic_archive+manga mapping left
the scan volumes unbadged. Anything in a manga library reads manga;
anything in a comics library reads comic. Verified: /dashboard lands 24
title="Manga" cover badges for the Manga library and the sections API
reports badge=manga for a fixed_layout item.

Ghost sidebar: the >=1024px media block applied body
padding-left:var(--sidebar-w) unconditionally, so pages that never
render the sidebar (logged-out pages, the audio player, the reader
itself) carried a phantom 16rem left margin that pushed content off
center on desktop widths. The body:has(.app-sidebar) rule below was
already the correct conditional source — it is now the only one.
Verified in the built stylesheet: exactly one such rule remains.

Infra: main.js ?v=3 stamp bump (reader.ts changed; hashed chunks churn
their URLs themselves).
2026-10-15 20:27:37 -04:00

1595 lines
42 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, drop the backdrop. No unconditional body
offset here — the body:has(.app-sidebar) rule below is the only source
of the sidebar padding, so pages that never render the sidebar
(logged-out, audio player, the reader itself) stay truly centered:
an unconditional rule reserved the sidebar's 16rem on every wide
viewport as an invisible left margin. */
@media (min-width: 1024px) {
.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);
}
/* Highlight extent-adjustment handles (objective 9): teardrop dots that
sit on the range boundary; mirrored pair — start points left, end
points right. */
.adjust-handle {
position: fixed;
width: 26px;
height: 26px;
/* Standard Android selection teardrops: both points face DOWN toward
the text. The square corner is the tip — start's bottom-left corner
rotated -45° sits straight down; end's bottom-right +45° likewise. */
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
background: #3b82f6;
border: 2px solid #ffffff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
pointer-events: auto;
cursor: grab;
touch-action: none;
}
.adjust-handle[data-which="end"] {
border-radius: 50% 50% 0 50%;
transform: rotate(45deg);
}
.adjust-handle[data-dragging] {
cursor: grabbing;
scale: 1.15;
}
/* PDF rect-edge adjust grips (fixed-layout highlights): short edge bars
centered on each rect's edge midpoint. Vertical edges drag in x,
horizontal edges in y — resize only, one rect per drag. */
.adjust-grip {
position: fixed;
width: 10px;
height: 30px;
border-radius: 5px;
background: #3b82f6;
border: 2px solid #ffffff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
pointer-events: auto;
cursor: ew-resize;
touch-action: none;
}
.adjust-grip[data-dir="h"] {
width: 30px;
height: 10px;
cursor: ns-resize;
}
.adjust-grip[data-dragging] {
scale: 1.1;
}
.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;
}
/* Markdown notes render through renderNoteHtml (markdown.ts) into
x-html containers at three sites (reader notes-drawer rows, the 📝
highlight note line, the banner's expanded body). Tailwind preflight
zeroes p/blockquote/list margins globally, which flattened paragraphs
single-spaced and left blockquotes/heads indistinguishable from body
text — this block restores block structure. em-based so it scales
with each site's text-xs/text-sm context. */
.note-rich p {
margin: 0;
}
.note-rich p + p {
margin-top: 0.6em;
}
.note-rich h1, .note-rich h2, .note-rich h3,
.note-rich h4, .note-rich h5, .note-rich h6 {
margin: 0.8em 0 0.4em;
font-weight: 600;
line-height: 1.3;
}
.note-rich h1 { font-size: 1.25em; }
.note-rich h2 { font-size: 1.15em; }
.note-rich h3, .note-rich h4 { font-size: 1.05em; }
.note-rich h5, .note-rich h6 { font-size: 1em; }
.note-rich blockquote {
margin: 0.6em 0;
padding: 0.1em 0 0.1em 0.75em;
border-left: 3px solid color-mix(in srgb, var(--text-secondary) 55%, transparent);
color: var(--text-secondary);
}
.note-rich ul, .note-rich ol {
margin: 0.6em 0;
padding-left: 1.4em;
}
.note-rich ul { list-style: disc; }
.note-rich ol { list-style: decimal; }
.note-rich li { margin: 0.15em 0; }
.note-rich li + li { margin-top: 0.25em; }
.note-rich li > p { margin: 0; }
.note-rich pre {
margin: 0.6em 0;
padding: 0.6em 0.75em;
overflow-x: auto;
border-radius: 0.375rem;
background-color: color-mix(in srgb, var(--bg-secondary) 80%, transparent);
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}
.note-rich pre code {
background: none;
padding: 0;
font-size: 0.9em;
}
.note-rich code {
font-size: 0.9em;
padding: 0.1em 0.3em;
border-radius: 0.25rem;
background-color: color-mix(in srgb, var(--bg-secondary) 80%, transparent);
}
.note-rich a {
text-decoration: underline;
text-underline-offset: 2px;
}
.note-rich hr {
margin: 0.8em 0;
}
.note-rich table {
margin: 0.6em 0;
border-collapse: collapse;
font-size: 0.95em;
}
.note-rich th, .note-rich td {
border: 1px solid var(--border);
padding: 0.25em 0.5em;
text-align: left;
}
.note-rich th { font-weight: 600; }
.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%);
}
}
}
/* Touch selection handles (custom gesture — the native ones are disabled) */
#selection-handles {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 55;
}
#selection-handles .sel-handle {
position: absolute;
top: 0;
left: 0;
width: 12px;
height: 24px;
pointer-events: auto;
touch-action: none;
background: #8ab4f8;
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
opacity: 0.95;
}
#selection-handles .sel-handle::before {
content: "";
position: absolute;
top: -4px;
left: 50%;
width: 2px;
height: 5px;
background: inherit;
transform: translateX(-50%);
}