Files
bookhoard/web/static/input.css
T
John O'Keefe f52bb6f073 feat(facets): all-facets panel with under-filter counts (faceted plan Phase 7)
The 9 Batch A facets could not be SET from the web UI, the facets
endpoint had no in-app consumer, and the registry's libraryTypes hints
rendered nowhere. This phase closes all three.

- ONE UNION-ALL sqlc query (FacetCountsUnderFilters) computes all 14
  listable facets in a single round trip: each arm omits its OWN
  facet's filter block and carries every other registered facet's
  block (copied mechanically from SearchMediaItemsUnified) — classic
  faceted semantics: with filters active each facet counts under the
  others; with none, blocks are neutral and the counts are the v1
  global-per-library ones. One query serves both cases. The 14
  per-column Batch C queries are deleted.
- Endpoint: no facet param → the new all-facets shape
  ({facets: {param: [{value, display?, count}]}}), filters bound via
  the shared ParseMediaSearchParams, library_id optional (the
  All-Libraries view, family lens still enforced); facet=X keeps the
  pinned single-facet shape (now routed through the same query with
  neutral filters).
- SSR partial GET /media-items/facet-panel: templ renders the drawer's
  value lists — registry order, Batch D library-type scoping
  (FacetsForLibraryType: manga_type on manga, reading_direction on
  manga+comics, narrator on audiobooks; All view = union), prettified
  genre labels + language display names, active values marked. Family
  lens: out-of-set explicit library_id is 403.
- Drawer wiring: the panel fetches on drawer open and after every
  apply with the CURRENT filter query string; a value click toggles
  that facet's comma-joined filter (hidden input where the drawer has
  no field — same as deep links) and re-applies the form; chips and
  URL stay in sync. Values are server-rendered (rendering doctrine);
  bookshelf.ts owns only the click/fetch orchestration.
- prettifyGenre moves to internal/utils as the canonical
  implementation (services' display enrichment shares it; templ wraps
  it; the TS port in bookshelf.ts is unchanged).
- Drift guard extended: FacetCountsUnderFiltersParams must carry every
  registry facet's param, the bools as pgtype.Bool.

Tests: under-filter exactness (own-block omission, OR-within,
AND-across), family lens (hidden values absent; 403 on both shapes),
all-shape + pinned-shape coexistence, partial rendering with Batch D
scoping + active marking + 403. Full container suite green except the
documented pre-existing TestAdminCreateUser red.
2026-10-20 08:24:36 -04:00

1947 lines
50 KiB
CSS

/* Tailwind CSS directives - required for framework */
@tailwind base;
@tailwind components;
@tailwind utilities;
/*
* Cross-document view transitions (the optional sweetener): modern
* browsers (Chrome 126+, Safari 18+) crossfade same-origin navigations —
* sidebar links and the series page's library-switch navigation get a
* subtle fade for free, with zero JavaScript. Firefox and older browsers
* simply navigate instantly; the rule is pure progressive enhancement.
*/
@view-transition {
navigation: auto;
}
@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 {
/* Native form controls (select dropdown lists, scrollbars, inputs)
follow the OS light scheme without this — the stats library
switcher's popup rendered white in every dark theme. Dark is the
document default (all themes except latte are dark). */
color-scheme: dark;
--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;
}
/* Omarchy dark themes, sorted by background luminance (darkest first).
Palettes: themes/<name>/colors.toml in basecamp/omarchy. Where the
upstream lighter_background matches the background (Last Horizon,
Solitude) the card tone is derived a step up instead. */
.theme-vantablack {
--bg-primary: #000000;
--bg-secondary: #1a1a1a;
--text-primary: #ffffff;
--text-secondary: #ececec;
--accent: #8d8d8d;
--border: #1a1a1a;
}
.theme-last-horizon {
--bg-primary: #0c0b0c;
--bg-secondary: #171516;
--text-primary: #fafcfb;
--text-secondary: #cfd3cd;
--accent: #b59790;
--border: #2b2729;
}
.theme-ethereal {
--bg-primary: #060b1e;
--bg-secondary: #131a3a;
--text-primary: #ffcead;
--text-secondary: #c9b8a6;
--accent: #7d82d9;
--border: #252e56;
}
.theme-hackerman {
--bg-primary: #0b0c16;
--bg-secondary: #151828;
--text-primary: #ddf7ff;
--text-secondary: #b5c5db;
--accent: #82fb9c;
--border: #1f253a;
}
.theme-matte-black {
--bg-primary: #121212;
--bg-secondary: #1e1e1e;
--text-primary: #bebebe;
--text-secondary: #8a8a8d;
--accent: #e68e0d;
--border: #2a2a2a;
}
.theme-solitude {
--bg-primary: #101315;
--bg-secondary: #171b1e;
--text-primary: #cacccc;
--text-secondary: #8a9199;
--accent: #798186;
--border: #262d31;
}
.theme-retro-82 {
--bg-primary: #05182e;
--bg-secondary: #0a2540;
--text-primary: #f6dcac;
--text-secondary: #a7c9c6;
--accent: #faa968;
--border: #134e5a;
}
.theme-osaka-jade {
--bg-primary: #111c18;
--bg-secondary: #23372b;
--text-primary: #c1c497;
--text-secondary: #81b8a8;
--accent: #509475;
--border: #32473b;
}
/* Omarchy maps Kanagawa's accent to its foreground; the UI uses
crystalBlue instead so links/buttons stay distinguishable from text. */
.theme-kanagawa {
--bg-primary: #1f1f28;
--bg-secondary: #223249;
--text-primary: #dcd7ba;
--text-secondary: #c8c093;
--accent: #7e9cd8;
--border: #363646;
}
.theme-miasma {
--bg-primary: #222222;
--bg-secondary: #2c2c2c;
--text-primary: #c2c2b0;
--text-secondary: #8a8a7e;
--accent: #78824b;
--border: #383838;
}
.theme-lumon {
--bg-primary: #16242d;
--bg-secondary: #1b2d40;
--text-primary: #d6e2ee;
--text-secondary: #4d86b0;
--accent: #8bc9eb;
--border: #243d56;
}
.theme-ristretto {
--bg-primary: #2c2525;
--bg-secondary: #3d2f2a;
--text-primary: #e6d9db;
--text-secondary: #c3b7b8;
--accent: #f38d70;
--border: #403e41;
}
.theme-gruvbox {
--bg-primary: #282828;
--bg-secondary: #3c3836;
--text-primary: #d4be98;
--text-secondary: #bdae93;
--accent: #7daea3;
--border: #504945;
}
.theme-everforest {
--bg-primary: #2d353b;
--bg-secondary: #343f44;
--text-primary: #d3c6aa;
--text-secondary: #9da9a0;
--accent: #7fbbb3;
--border: #3d484d;
}
.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 {
color-scheme: light;
--bg-primary: #eff1f5;
--bg-secondary: #ffffff;
--text-primary: #4c4f69;
--text-secondary: #6c6f85;
--accent: #d20f39;
--border: #dcdfe6;
}
/* Light themes. Each carries color-scheme: light so native form
controls, scrollbars and UA popups render light. Backgrounds follow
each theme's canonical palette; the card tone mirrors what the theme
itself does (elevated white for screen-like themes, a darker paper
tone for the paper ones — Solarized set that convention). */
.theme-tokyo-night-day {
color-scheme: light;
--bg-primary: #e1e2e7;
--bg-secondary: #ffffff;
--text-primary: #3760bf;
--text-secondary: #6172b0;
--accent: #2e7de9;
--border: #c4c8da;
}
.theme-solarized-light {
color-scheme: light;
--bg-primary: #fdf6e3;
--bg-secondary: #eee8d5;
--text-primary: #586e75;
--text-secondary: #93a1a1;
--accent: #268bd2;
--border: #ddd6c1;
}
.theme-one-light {
color-scheme: light;
--bg-primary: #fafafa;
--bg-secondary: #ffffff;
--text-primary: #383a42;
--text-secondary: #696c77;
--accent: #4078f2;
--border: #dfdfdf;
}
.theme-ayu-light {
color-scheme: light;
--bg-primary: #fafafa;
--bg-secondary: #f3f4f5;
--text-primary: #5c6166;
--text-secondary: #8a9199;
--accent: #ff9933;
--border: #e2e3e5;
}
.theme-github-light {
color-scheme: light;
--bg-primary: #ffffff;
--bg-secondary: #f6f8fa;
--text-primary: #24292f;
--text-secondary: #57606a;
--accent: #0969da;
--border: #d0d7de;
}
.theme-gruvbox-light {
color-scheme: light;
--bg-primary: #fbf1c7;
--bg-secondary: #f2e5bc;
--text-primary: #3c3836;
--text-secondary: #7c6f64;
--accent: #076678;
--border: #d5c4a1;
}
.theme-everforest-light {
color-scheme: light;
--bg-primary: #fdf6e3;
--bg-secondary: #f4f0d9;
--text-primary: #5c6a72;
--text-secondary: #708089;
--accent: #3a94c5;
--border: #d5cfaf;
}
.theme-kanagawa-lotus {
color-scheme: light;
--bg-primary: #f2ecbc;
--bg-secondary: #e5ddb0;
--text-primary: #545464;
--text-secondary: #716e61;
--accent: #4d699b;
--border: #d5cea3;
}
/* Omarchy light themes, sorted by background luminance (darkest first). */
.theme-rose-pine {
color-scheme: light;
--bg-primary: #faf4ed;
--bg-secondary: #fffaf3;
--text-primary: #575279;
--text-secondary: #797593;
--accent: #56949f;
--border: #dfdad9;
}
.theme-lupine {
color-scheme: light;
--bg-primary: #fafafa;
--bg-secondary: #ffffff;
--text-primary: #212121;
--text-secondary: #757575;
--accent: #3264eb;
--border: #d0d0d0;
}
.theme-flexoki-light {
color-scheme: light;
--bg-primary: #fffcf0;
--bg-secondary: #f2efe4;
--text-primary: #100f0f;
--text-secondary: #6f6e69;
--accent: #205ea6;
--border: #cecdc3;
}
/* Omarchy's pure monochrome theme: its accent is deliberately grey. */
.theme-white {
color-scheme: light;
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--text-primary: #000000;
--text-secondary: #808080;
--accent: #6e6e6e;
--border: #e8e8e8;
}
/*
* 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);
}
/* Softer shadows on light themes — dark shadows tuned for dark themes
look harsh on paper backgrounds. */
.theme-catppuccin-latte,
.theme-tokyo-night-day,
.theme-solarized-light,
.theme-one-light,
.theme-ayu-light,
.theme-github-light,
.theme-gruvbox-light,
.theme-everforest-light,
.theme-kanagawa-lotus,
.theme-rose-pine,
.theme-lupine,
.theme-flexoki-light,
.theme-white {
--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);
}
/* Select popup entries follow the theme (the closed control is
themed by its own classes; the option LIST is what UA-styles light) */
select option {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
/* 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;
}
/* Active half of the Appearance panel's Dark/Light mode filter. */
.mode-toggle-active {
background-color: var(--accent-muted);
color: var(--text-primary);
}
/* 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);
}
/* ---------- Facet panel (filter drawer, Phase 7) ---------- */
/* The values are templ-rendered (templates/facet_panel.templ); this
is look only. A value button is plain until active: active means
"currently in that facet's filter" — accent-tinted like a chip. */
.facet-section-summary {
list-style: none;
}
.facet-section-summary::-webkit-details-marker {
display: none;
}
.facet-section-summary::before {
content: "▸";
display: inline-block;
margin-right: 0.5rem;
color: var(--text-secondary);
transition: transform 0.15s ease;
font-size: 0.7rem;
}
.facet-section[open] .facet-section-summary::before {
transform: rotate(90deg);
}
.facet-value-btn {
background-color: transparent;
}
.facet-value-btn:hover {
background-color: var(--surface-hover);
}
.facet-value-btn.facet-value-active {
background-color: var(--accent-muted);
color: var(--accent);
}
.facet-value-btn.facet-value-active span {
color: var(--accent);
}
.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%);
}