/* web/static/css/library-card.css: the Free Library section on /app (heading, search,
   grid), the library card, the desktop peek and the touch Quick look. Uses the site
   tokens from _head_styles.html and input.css (the --color- and --font- families).
   Replaces app.html's inline .novel-grid and .novel-card rules and audio-badge.css on /app.
   Rules: nothing on the art at rest; no coloured text; no box; no backdrop-filter or
   will-change on cards; only transform and opacity animate; hover lives behind a fine
   pointer. Markup: _novel_card.html (the card) and app.html (the once-per-page parts). */

:root {
  --lc-ease: cubic-bezier(.2, .8, .2, 1);
  --lc-ring: #F5F5F4;
}

/* The cover light reaches up to 18% past the outer columns. Clip it on BOTH html and body
   (clip, never hidden: no scroll container, so the sticky navbar keeps working, and a phone's
   layout viewport never widens; either one alone lets mobile Chrome widen to 405px once the
   Quick look's scroll lock toggles html overflow). */
html, body { overflow-x: clip; }
html.ql-lock { overflow: hidden; }

/* ── Section header: serif heading with the italic serif ampersand, lede, search ── */
.library-head { display: flex; flex-direction: column; gap: 16px; margin-bottom: 20px; }
.library-title { margin: 0; font: 400 26px/1.15 var(--font-heading); color: var(--color-text-heading); letter-spacing: .005em; }
.library-title .amp { font-style: italic; padding: 0 .04em; }
.library-lede { margin: 6px 0 0; font: 400 15px/22px var(--font-body); color: var(--color-text-muted); max-width: 46ch; }
.library-search { position: relative; display: block; }
.library-search svg { position: absolute; left: 14px; top: 50%; width: 18px; height: 18px; transform: translateY(-50%); color: var(--color-text-muted); pointer-events: none; }
.library-search input {
  display: block; width: 100%; height: 44px; margin: 0; padding: 0 16px 0 42px; border: 0; border-radius: 9999px;
  background: var(--color-bg-subtle); color: var(--color-text-heading);
  box-shadow: inset 0 0 0 1px var(--color-border-default);
  font: 400 15px/1 var(--font-body); outline: none; -webkit-appearance: none; appearance: none;
  color-scheme: dark;                                        /* the native clear control neutral, not UA blue */
}
.library-search input::placeholder { color: var(--color-text-muted); opacity: 1; }
.library-search input:focus-visible { box-shadow: inset 0 0 0 1px var(--color-primary), 0 0 0 3px rgb(13 148 136 / .25); }
.library-search input::-webkit-search-decoration { -webkit-appearance: none; }
/* Phones and tablets only: the social links (the floating rail shows from 1024px).
   Monochrome stone, one size, one shape, so they add no hue to the section. */
.library-social { display: flex; align-items: center; gap: 12px; margin-top: 12px; }
.library-social__label { font: 500 14px/20px var(--font-body); color: var(--color-text-muted); }
.library-social a {
  display: grid; place-items: center; width: 44px; height: 44px; border-radius: 50%;
  color: var(--color-text-body); background: var(--color-bg-subtle);
  box-shadow: inset 0 0 0 1px var(--color-border-default);
  -webkit-tap-highlight-color: transparent;
}
.library-social a:focus-visible { outline: 2px solid var(--lc-ring); outline-offset: 2px; }
@media (hover: hover) { .library-social a:hover { background: var(--color-bg-muted); color: var(--color-text-heading); } }
.library-social svg { width: 16px; height: 16px; }
.library-genres { margin-bottom: 26px; }
@media (min-width: 1024px) {
  /* The floating social rail (fixed, 24px from the right edge, 48px wide, shown from
     1024px) must never sit on a cover: where the container's outer margin is narrower
     than the rail's zone plus 16px, the section stops short of it. 100% is main's
     content box, so (100vw - 100% - 32px) / 2 is the container's outer margin.
     At 1440 this is 0; at 1024 and 1280 it is 72px. */
  .library { padding-right: max(0px, calc(72px - (100vw - 100% - 32px) / 2)); }
  .library-head { flex-direction: row; align-items: flex-end; justify-content: space-between; margin-bottom: 24px; }
  .library-title { font-size: 32px; }
  .library-lede { font-size: 16px; max-width: none; }
  .library-search { width: 300px; flex: none; }
  .library-social { display: none; }
  .library-genres { margin-bottom: 36px; }
}

/* Ambient wash: ONE tint per screen, from the lead (or hovered) cover. Two fixed layers
   crossfade, so only opacity animates. Markup: <div class="library-wash" aria-hidden="true"><i class="on"></i><i></i></div> */
.library-wash { position: fixed; inset: 0; z-index: -1; pointer-events: none; }
.library-wash i {
  position: absolute; inset: 0; opacity: 0;
  background: radial-gradient(72% 52% at var(--ax, 30%) -6%, rgb(var(--amb, 120 113 108) / .15), transparent 72%);
  transition: opacity .7s ease;
}
.library-wash i.on { opacity: 1; }

/* ── Grid: explicit columns; covers stay 170–232px everywhere ── */
.novel-grid {
  --cols: 2; --gap-x: 14px; --gap-y: 30px;
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
  column-gap: var(--gap-x); row-gap: var(--gap-y); align-items: start;
  /* One stacking context for the whole grid: every card's light sits behind EVERY cover. */
  position: relative; isolation: isolate;
}
@media (min-width: 640px)  { .novel-grid { --cols: 3; --gap-x: 20px; --gap-y: 38px; } }
@media (min-width: 1024px) { .novel-grid { --cols: 4; --gap-x: 24px; --gap-y: 42px; } }
@media (min-width: 1280px) { .novel-grid { --cols: 5; --gap-x: 28px; --gap-y: 46px; } }
@media (min-width: 1536px) { .novel-grid { --cols: 6; } }
.novel-grid[aria-busy="true"] { opacity: .5; transition: opacity .15s ease; }   /* genre pick loading (kept) */

/* Shared baselines where the row carries a control column: on touch every meta line ends in
   the "More" slot, so each card spans three grid rows (cover / title / meta) via subgrid and
   every meta line in a row sits on one line. With a fine pointer title + meta stay a tight
   pair (ragged rows, no reserved blank line). No subgrid: plain grid above. */
@media (hover: none), (pointer: coarse) {
  @supports (grid-template-rows: subgrid) {
    .novel-grid { row-gap: 0; }
    .novel-grid > .novel-card { display: grid; grid-row: span 3; grid-template-rows: subgrid; row-gap: 0; margin-bottom: var(--gap-y); }
    .novel-grid .card-title, .novel-grid .card-meta { align-self: start; }
  }
}

/* ── The card: the cover IS the card ── */
.novel-card { position: relative; min-width: 0; --g: .6; }

.card-art { position: relative; aspect-ratio: 1; }
/* Light 1, halo: the cover's colour around and below it. Gradient on a pseudo-element:
   no filter, no blurred box-shadow, no promoted layer at rest. --g is per cover (server). */
.card-art::before {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  inset: 2% -18% -24% -18%;
  background: radial-gradient(closest-side,
    rgb(var(--glow) / .72) 0%, rgb(var(--glow) / .52) 56%, rgb(var(--glow) / .2) 80%, transparent 100%);
  opacity: var(--g);
  transform-origin: 50% 64%;
  transition: opacity .32s ease, transform .32s var(--lc-ease);
}
/* Light 2, floor pool: where the light lands, just under the bottom edge. */
.card-art::after {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  left: 3%; right: 3%; bottom: -24px; height: 48px;
  background: radial-gradient(closest-side, rgb(var(--glow) / .62), rgb(var(--glow) / .22) 55%, transparent);
  opacity: var(--g);
  transition: opacity .32s ease, transform .5s var(--lc-ease);
}
.card-cover {
  position: absolute; inset: 0; border-radius: 8px;
  background: var(--tint);                                   /* placeholder already in the book's colour */
  box-shadow: 0 2px 4px rgb(0 0 0 / .45), 0 14px 30px -10px rgb(0 0 0 / .7);
  transition: transform .32s var(--lc-ease);
}
.card-cover::after {                                         /* deeper lift shadow, faded in */
  content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit; pointer-events: none;
  box-shadow: 0 26px 40px -22px rgb(0 0 0 / .55);
  opacity: 0; transition: opacity .32s ease;
}
.card-frame { position: absolute; inset: 0; border-radius: inherit; overflow: hidden; }
.card-frame img { display: block; width: 100%; height: 100%; object-fit: cover; }
.card-frame::after {                                         /* hairline + faint top light, static */
  content: ""; position: absolute; inset: 0; z-index: 1; border-radius: inherit; pointer-events: none;
  background: linear-gradient(180deg, rgb(255 255 255 / .06), transparent 26%);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .08), inset 0 1px 0 rgb(255 255 255 / .08);
}
.card-frame::before {                                        /* intent hairline .08 -> .18 */
  content: ""; position: absolute; inset: 0; z-index: 2; border-radius: inherit; pointer-events: none;
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .18);
  opacity: 0; transition: opacity .32s ease;
}
.card-sheen {
  position: absolute; inset: -2px; z-index: 1; pointer-events: none;
  background: linear-gradient(112deg, transparent 36%, rgb(255 255 255 / .15) 50%, transparent 64%);
  transform: translateX(-120%);
}
.card-bloom {                                                /* ONE blurred copy, desktop intent only (JS) */
  position: absolute; inset: -4% -4% -8%; z-index: -1; border-radius: 24px; pointer-events: none;
  background-size: cover; background-position: center;
  filter: blur(32px) saturate(1.3);
  opacity: 0; transition: opacity .4s ease;
}
.card-bloom.on { opacity: .55; }

.card-title {
  margin: 11px 0 0;
  font: 400 17px/21px var(--font-heading); color: var(--color-text-heading); letter-spacing: .004em;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
  text-wrap: balance;
}
.card-link {
  color: inherit; outline: none; -webkit-tap-highlight-color: transparent;
  text-decoration: underline 1px; text-decoration-color: transparent; text-underline-offset: 4px;
  transition: text-decoration-color .2s ease;
}
.card-link:hover { color: inherit; }                         /* beat any global a:hover colour */
.card-link::after { content: ""; position: absolute; inset: -6px; z-index: 1; border-radius: 14px; }   /* stretched link */
.card-link:focus-visible { text-decoration-color: rgb(245 245 244 / .45); }
.card-link:focus-visible::after { outline: 2px solid var(--lc-ring); outline-offset: 2px; }

.card-meta {
  margin: 4px 0 0; display: flex; align-items: center; gap: 7px; min-width: 0;
  font: 500 14px/20px var(--font-body); color: var(--color-text-muted);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.card-meta .i { width: 16px; height: 16px; flex: none; }
.card-meta__t { overflow: hidden; text-overflow: ellipsis; }
.card-meta b { font-weight: 500; color: var(--color-text-body); }   /* "Full cast": one shade lighter, no chip */

/* The one action slot, right end of the meta row. Touch only in v1 (opens Quick look);
   phase 2: the owner menu on Novelshelf/Profile uses the same slot. */
.card-more { display: none; }
@media (hover: none), (pointer: coarse) {
  .card-meta { padding-right: 24px; }
  .card-more {
    display: grid; place-items: center;
    position: absolute; z-index: 2; right: -13px; bottom: -12px; width: 44px; height: 44px;
    padding: 0; border: 0; background: none; border-radius: 50%;
    color: rgb(168 162 158 / .78);                           /* stone-400/.78, never brighter */
    -webkit-tap-highlight-color: transparent; cursor: pointer;
  }
  .card-more svg { width: 18px; height: 18px; }
  .card-more:active { color: var(--color-text-body); }
  .card-more:focus-visible { outline: 2px solid var(--lc-ring); outline-offset: -6px; }
  .novel-card:has(.card-link:active) .card-cover { transform: scale(.975); transition-duration: .09s; }
  .novel-card:has(.card-link:active) .card-art::before,
  .novel-card:has(.card-link:active) .card-art::after { opacity: 1; transition-duration: .09s; }
}

/* Desktop intent: lift, light, sheen, underline. */
@media (hover: hover) and (pointer: fine) {
  .card-link { cursor: pointer; }
  .novel-card:hover .card-cover,
  .novel-card:has(.card-link:focus-visible) .card-cover { transform: translateY(-6px); }
  .novel-card:hover .card-cover::after,
  .novel-card:has(.card-link:focus-visible) .card-cover::after { opacity: 1; }
  .novel-card:hover .card-art::before,
  .novel-card:has(.card-link:focus-visible) .card-art::before { opacity: 1; transform: translateY(-3px) scale(1.05); }
  .novel-card:hover .card-art::after,
  .novel-card:has(.card-link:focus-visible) .card-art::after { opacity: 1; transform: scaleX(1.08); }
  .novel-card:hover .card-frame::before,
  .novel-card:has(.card-link:focus-visible) .card-frame::before { opacity: 1; }
  .novel-card:hover .card-sheen,
  .novel-card:has(.card-link:focus-visible) .card-sheen { transform: translateX(120%); transition: transform .9s cubic-bezier(.3, .6, .2, 1); }
  .novel-card:hover .card-link { text-decoration-color: rgb(245 245 244 / .45); }
  .novel-card:has(.card-link:active) .card-cover { transform: translateY(-6px) scale(.98); transition-duration: .09s; }
}

/* Entrance (JS adds the classes): below-the-fold and appended cards rise once. */
.novel-card.pre { opacity: 0; transform: translateY(14px); }
.novel-card.rise { opacity: 1; transform: none; transition: opacity .42s ease, transform .42s var(--lc-ease); transition-delay: var(--d, 0ms); }

@media (min-width: 640px) {
  .card-title { margin-top: 12px; font-size: 18px; line-height: 22px; }
  .card-meta { margin-top: 5px; }
}
@media (min-width: 1024px) {
  .card-title { margin-top: 14px; font-size: 20px; line-height: 24px; }
  .card-meta { margin-top: 6px; }
}
@media (max-width: 639px) {
  .card-cover { border-radius: 7px; }
  .card-art::after { bottom: -18px; height: 36px; }
}

/* ── Peek: one shared popover beside the hovered cover (fine pointer only) ── */
.card-peek {
  position: fixed; z-index: 20; top: 0; left: 0; width: min(400px, calc(100vw - 32px));
  padding: 20px 24px 24px; border-radius: 18px;
  background:
    radial-gradient(90% 75% at var(--wx, 0%) 0%, rgb(var(--glow, 120 113 108) / .34), transparent 64%),
    rgb(22 22 25 / .88);
  -webkit-backdrop-filter: blur(24px) saturate(1.4); backdrop-filter: blur(24px) saturate(1.4);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .08), inset 0 1px 0 rgb(var(--glow, 255 255 255) / .5), 0 24px 64px rgb(0 0 0 / .55), 0 2px 6px rgb(0 0 0 / .35);
  opacity: 0; visibility: hidden; transform: translateX(var(--from, -8px));
  transition: opacity .12s ease, transform .12s ease, visibility 0s linear .12s;
  cursor: pointer;
}
@media not ((hover: hover) and (pointer: fine)) { .card-peek { display: none; } }   /* 400px: never on a phone */
.card-peek.open { opacity: 1; visibility: visible; transform: none; transition: opacity .18s ease-out, transform .18s ease-out, visibility 0s; }
.credits { margin: 0; display: flex; align-items: baseline; gap: 16px; min-width: 0; }
.credits .by { font: italic 400 18px/24px var(--font-heading); color: var(--color-text-body); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.credits .genre { margin-left: auto; flex: none; font: 500 14px/20px var(--font-body); color: var(--color-text-muted); }
.peek-synopsis { margin: 12px 0 0; font: 400 15px/24px var(--font-body); color: var(--color-text-body); text-wrap: pretty; }
.peek-synopsis.clamp, .ql-synopsis.clamp { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 8; overflow: hidden; }
.peek-synopsis .hook, .ql-synopsis .hook { color: var(--color-text-heading); }

/* ── Quick look: a real modal <dialog> (showModal: page behind inert, Tab stays inside) ── */
.quick-look {
  position: fixed; inset: 0; width: 100%; height: 100%; max-width: none; max-height: none;
  margin: 0; padding: 0; border: 0; background: transparent; color: inherit; overflow: hidden; overscroll-behavior: contain;
}
.quick-look::backdrop { background: transparent; }
.ql-scrim { position: absolute; inset: 0; background: rgb(0 0 0 / .58); opacity: 0; transition: opacity .24s ease; }
.ql-sheet {
  position: absolute; left: 0; right: 0; bottom: 0; max-height: 86vh; overflow: auto; overscroll-behavior: contain;
  padding: 10px 20px calc(20px + env(safe-area-inset-bottom)); border-radius: 22px 22px 0 0;
  background: radial-gradient(110% 60% at 50% 0%, rgb(var(--glow, 120 113 108) / .26), transparent 70%), rgb(20 20 23);
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .10), 0 -20px 60px rgb(0 0 0 / .5);
  transform: translateY(102%); transition: transform .34s var(--lc-ease); outline: none;
}
@media (min-width: 640px) {                                  /* tablets: a centred card-width sheet */
  .ql-sheet { left: 50%; right: auto; width: 560px; margin-left: -280px; }
}
.quick-look.open .ql-scrim { opacity: 1; }
.quick-look.open .ql-sheet { transform: none; }
.ql-grip { width: 40px; height: 5px; margin: 0 auto 14px; border-radius: 3px; background: rgb(255 255 255 / .22); }
.ql-head { display: grid; grid-template-columns: 112px minmax(0, 1fr); gap: 16px; align-items: end; padding-right: 8px; }
.ql-cover { width: 112px; height: 112px; border-radius: 8px; object-fit: cover; display: block; background: var(--tint, #18181b); box-shadow: 0 2px 4px rgb(0 0 0 / .45), 0 16px 30px -10px rgb(0 0 0 / .7), 0 0 0 1px rgb(255 255 255 / .08); }
.ql-title { margin: 0; font: 400 24px/28px var(--font-heading); color: var(--color-text-heading); text-wrap: balance; }
.ql-head .credits { margin-top: 6px; flex-wrap: wrap; row-gap: 2px; }
.ql-head .credits .by { white-space: normal; }
.ql-head .credits .genre { margin-left: 0; }
.ql-meta { margin: 6px 0 0; display: flex; align-items: center; gap: 7px; font: 500 14px/20px var(--font-body); color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
.ql-meta .i { width: 16px; height: 16px; flex: none; }
.ql-meta b { font-weight: 500; color: var(--color-text-body); }
.ql-synopsis { margin: 18px 0 0; font: 400 16px/25px var(--font-body); color: var(--color-text-body); text-wrap: pretty; }
.ql-actions { margin-top: 22px; display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 10px; }   /* equal-width pair in phase 2 */
.ql-btn {
  height: 50px; border-radius: 14px; border: 0; display: flex; align-items: center; justify-content: center; gap: 8px;
  font: 600 16px/1 var(--font-body); text-decoration: none; cursor: pointer;
  background: rgb(255 255 255 / .08); color: var(--color-text-heading); box-shadow: inset 0 0 0 1px rgb(255 255 255 / .12);
}
.ql-btn--primary { background: var(--color-primary); color: #fff; box-shadow: inset 0 1px 0 rgb(255 255 255 / .16); }
.ql-btn[hidden] { display: none; }
.ql-btn svg { width: 18px; height: 18px; }
.ql-btn:focus-visible, .ql-close:focus-visible { outline: 2px solid var(--lc-ring); outline-offset: 2px; }
.ql-close { position: absolute; top: 12px; right: 14px; width: 36px; height: 36px; border: 0; border-radius: 50%; display: grid; place-items: center; background: rgb(255 255 255 / .08); color: var(--color-text-body); cursor: pointer; }
.ql-close svg { width: 16px; height: 16px; }
.ql-close::before { content: ""; position: absolute; inset: -4px; }   /* 44px target */

@media (prefers-reduced-motion: reduce) {
  .card-cover, .card-sheen, .card-art::before, .card-art::after { transition: none !important; }
  .novel-card:hover .card-cover, .novel-card:has(.card-link:focus-visible) .card-cover,
  .novel-card:has(.card-link:active) .card-cover { transform: none !important; }
  .card-sheen, .card-bloom { display: none; }
  .novel-card.pre { opacity: 1; transform: none; }
  .card-art::before, .card-art::after { transform: none !important; transition: opacity .12s !important; }
  .card-peek, .card-peek.open { transform: none; transition: opacity .12s, visibility 0s; }
  .ql-sheet { transition: none; }
}

/* ── Phase 2: shelf + profile ── */
/* Novelshelf (Generated + Bookmarked) and Profile (recent novels) render the same card.
   Markup: novelshelf.html, profile.html, the owner card in _novel_card.html
   ([data-owner], [data-private]); behaviour: library-owner-menu.js. */

/* The card's action wrapper adds no box: the slot keeps its own place at the right end
   of the meta row (and, on touch, stays out of the card's subgrid rows). */
.card-actions { display: contents; }

/* Owner cards: the slot is the owner menu (aria-haspopup="menu"), on EVERY pointer --
   owners need it on desktop too. Same bare ellipsis, same 44px target, same stone-400/.78. */
.novel-card[data-owner] .card-meta { padding-right: 24px; }
.novel-card[data-owner] .card-more {
  display: grid; place-items: center;
  position: absolute; z-index: 2; right: -13px; bottom: -12px; width: 44px; height: 44px;
  padding: 0; border: 0; background: none; border-radius: 50%;
  color: rgb(168 162 158 / .78);
  -webkit-tap-highlight-color: transparent; cursor: pointer;
  transition: color .15s ease;
}
.novel-card[data-owner] .card-more svg { width: 18px; height: 18px; }
.novel-card[data-owner] .card-more:focus-visible { outline: 2px solid var(--lc-ring); outline-offset: -6px; }
.novel-card[data-owner] .card-more[aria-expanded="true"],
.novel-card[data-owner] .card-more:active { color: var(--color-text-body); }
@media (hover: hover) and (pointer: fine) {
  .novel-card[data-owner] .card-more:hover { color: var(--color-text-body); }
}

/* A private book (one outside the public library) shows a stone-300 lock in place of
   its kind icon, on every card and at every width: [lock] Full cast · 11h 52m,
   [lock] 14 chapters. One form for the whole grid: the word form ("Private · ...")
   would fit only some cards (not a phone's 172px card, not an audiobook whose meta
   line keeps room for the desktop listen disc beside the owner menu), and a row that
   mixed the word and the lock would say one fact two ways. The word stays in the
   markup for Quick look (library-cards.js clones the line into the wide sheet) and
   for screen readers ("Private. ..."). */
.card-meta .i-lock { display: none; }
.novel-card[data-private] .card-meta > .i { display: none; }
.novel-card[data-private] .card-meta > .i-lock { display: block; color: var(--color-text-body); }
.novel-card[data-private] .card-meta .card-private { display: none; }

/* An owner row ends every meta line in the slot, so its rows share baselines on desktop
   too (the touch rule above, for every pointer): cover / title / meta are 3 subgrid rows. */
@supports (grid-template-rows: subgrid) {
  .novel-grid--owner { row-gap: 0; }
  .novel-grid--owner > .novel-card { display: grid; grid-row: span 3; grid-template-rows: subgrid; row-gap: 0; margin-bottom: var(--gap-y); }
  .novel-grid--owner .card-title, .novel-grid--owner .card-meta { align-self: start; }
  /* No extra gap after the last row. The grid's box then reaches over what follows it
     (Load More), so the box itself lets clicks through; its cards still take them. */
  .novel-grid--owner { margin-bottom: calc(-1 * var(--gap-y)); pointer-events: none; }
  .novel-grid--owner > .novel-card { pointer-events: auto; }
}

/* The owner menu: ONE element (library-owner-menu.js), fixed beside the slot. The peek's
   glass, quieter; neutral items (no coloured text, Delete included: its confirmation
   carries the warning). Only opacity and transform animate. */
.owner-menu {
  position: fixed; z-index: 30; top: 0; left: 0; min-width: 212px; padding: 6px;
  border-radius: 14px;
  background: rgb(26 26 29 / .94);
  -webkit-backdrop-filter: blur(20px) saturate(1.3); backdrop-filter: blur(20px) saturate(1.3);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .10), 0 18px 48px rgb(0 0 0 / .55), 0 2px 6px rgb(0 0 0 / .35);
  opacity: 0; transform: translateY(-4px) scale(.98); transform-origin: 100% 0;
  transition: opacity .14s ease, transform .14s var(--lc-ease);
}
.owner-menu[data-side="above"] { transform-origin: 100% 100%; transform: translateY(4px) scale(.98); }
.owner-menu.open { opacity: 1; transform: none; }
.owner-menu__item {
  display: flex; align-items: center; gap: 12px; width: 100%; height: 44px; margin: 0; padding: 0 16px 0 12px;
  border: 0; border-radius: 9px; background: none; color: var(--color-text-heading);
  font: 500 15px/1 var(--font-body); text-align: left; white-space: nowrap; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.owner-menu__item + .owner-menu__item { margin-top: 2px; }
.owner-menu__item svg { width: 18px; height: 18px; flex: none; color: var(--color-text-muted); }
.owner-menu__item:hover, .owner-menu__item:focus-visible { background: rgb(255 255 255 / .08); outline: none; }
.owner-menu__item:focus-visible { box-shadow: inset 0 0 0 2px var(--lc-ring); }

/* Quick look on an owner card: the created date, under the meta line. */
.ql-date { margin: 4px 0 0; font: 500 14px/20px var(--font-body); color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
.ql-date[hidden] { display: none; }

/* Profile: a compact grid -- 2 columns, 3 from 768px, 4 from 1024px -- inside the profile
   panel (on a phone the panel steps back so the cards keep the library's 172px: profile.html). */
.novel-grid--compact { --cols: 2; --gap-x: 14px; --gap-y: 30px; }
@media (min-width: 768px)  { .novel-grid--compact { --cols: 3; --gap-x: 20px; --gap-y: 38px; } }
@media (min-width: 1024px) { .novel-grid--compact { --cols: 4; --gap-x: 28px; --gap-y: 46px; } }

/* Novelshelf header: the library's search pill, at a fixed width beside the jump links. */
.shelf-search { width: 100%; }
@media (min-width: 640px) { .shelf-search { width: 280px; flex: none; } }

@media (prefers-reduced-motion: reduce) {
  .owner-menu, .owner-menu[data-side="above"] { transform: none; transition: opacity .12s; }
}

/* ── Phase 2: player ── */
/* The card's action slot (_novel_card.html) is one display: contents wrapper (defined
   in the shelf section above), so the touch subgrid keeps its three rows and the
   controls still place against the card. */

/* Desktop listen disc: the same action slot as the touch "More", right end of the meta
   row, never on the art. Neutral glass, teal only under its own hover. Hidden with
   visibility at rest (out of the tab order); revealed by hover, or by keyboard focus
   in the card (:focus-visible, so a mouse click that leaves focus behind never pins it
   on a resting card). Only once library-player.js is running (html.has-library-player),
   so the disc is never a dead button. */
.card-listen { display: none; }
@media (hover: hover) and (pointer: fine) {
  .novel-card:has(.card-listen) .card-meta { padding-right: 34px; }   /* the meta never runs under the disc */
  .card-listen {
    display: grid; place-items: center;
    position: absolute; z-index: 2; right: -4px; bottom: -6px; width: 32px; height: 32px;
    padding: 0; border: 0; border-radius: 50%; cursor: pointer;
    color: var(--color-text-heading); background: rgb(255 255 255 / .08); box-shadow: inset 0 0 0 1px rgb(255 255 255 / .14);
    opacity: 0; visibility: hidden; transform: scale(.9);
    transition: opacity .18s ease, transform .18s var(--lc-ease), visibility 0s linear .18s, background-color .15s ease, box-shadow .15s ease;
  }
  .card-listen::before { content: ""; position: absolute; inset: -6px; border-radius: 50%; }   /* 44px target */
  .card-listen svg { width: 13px; height: 13px; margin-left: 2px; }
  .has-library-player .novel-card:hover .card-listen,
  .has-library-player .novel-card:has(:focus-visible) .card-listen { opacity: 1; visibility: visible; transform: none; transition-delay: 0s; }
  .card-listen:hover { background: var(--color-primary); color: #fff; box-shadow: inset 0 1px 0 rgb(255 255 255 / .2); }
  .has-library-player .novel-card .card-listen:active { transform: scale(.92); transition-duration: .09s; }
  .card-listen:focus-visible { outline: 2px solid var(--lc-ring); outline-offset: 2px; }
  /* Owner cards (Novelshelf / Profile): the slot is the always-visible owner menu (the
     .card-more[data-owner-menu] ⋯), so the disc appears just left of it, clear of its
     44px target, and the meta line reserves room for both. */
  .novel-card:has(.card-more[data-owner-menu]) .card-listen { right: 36px; }
  .novel-card:has(.card-more[data-owner-menu]):has(.card-listen) .card-meta { padding-right: 72px; }
  /* While the disc shows, the owner ⋯ beside it wears the same 32px glass disc (same
     centre line, same glyph colour), so the two read as one pair of equal controls.
     At rest the ⋯ stays bare. */
  .novel-card:has(.card-listen) .card-more[data-owner-menu]::before {
    content: ""; position: absolute; inset: 6px; z-index: -1; border-radius: 50%;
    background: rgb(255 255 255 / .08); box-shadow: inset 0 0 0 1px rgb(255 255 255 / .14);
    opacity: 0; transform: scale(.9);
    transition: opacity .18s ease, transform .18s var(--lc-ease), background-color .15s ease;
  }
  .has-library-player .novel-card:has(.card-listen):hover .card-more[data-owner-menu],
  .has-library-player .novel-card:has(.card-listen):has(:focus-visible) .card-more[data-owner-menu] { color: var(--color-text-heading); }
  .has-library-player .novel-card:has(.card-listen):hover .card-more[data-owner-menu]::before,
  .has-library-player .novel-card:has(.card-listen):has(:focus-visible) .card-more[data-owner-menu]::before { opacity: 1; transform: none; }
  .has-library-player .novel-card:has(.card-listen) .card-more[data-owner-menu]:hover::before { background: rgb(255 255 255 / .14); }
}
/* A narrow desktop window (three columns under 768px): an owner card's meta line cannot
   keep room for both discs without clipping its facts, so there the owner slot keeps
   only its ⋯, and Listen is one step away in the menu's Quick Look ([Listen] [Open book]). */
@media (hover: hover) and (pointer: fine) and (max-width: 767.98px) {
  .novel-card:has(.card-more[data-owner-menu]) .card-listen { display: none; }
  .novel-card:has(.card-more[data-owner-menu]):has(.card-listen) .card-meta { padding-right: 24px; }
  .novel-card .card-more[data-owner-menu]::before { display: none; }
}

/* Quick look's [Listen] keeps the shared .ql-btn--primary look; [Open book] turns neutral
   beside it (library-cards.js), and .ql-actions makes the two equal width. */

/* The mini-player: one shared bar (_library_overlays.html), inert and hidden while closed.
   Bottom 16px + safe area, lifted above the cookie / app banners when they show (JS sets
   --mp-above). min(560px, 100% - 24px) x 68. Sits under the Quick look (top layer) and
   the consent banner (z-50), over the app banner (z-40) and the peek (z-20). */
.mini-player {
  position: fixed; z-index: 45; left: 50%;
  bottom: max(calc(16px + env(safe-area-inset-bottom)), calc(var(--mp-above, 0px) + 12px));
  width: min(560px, calc(100% - 24px)); height: 68px; padding: 0 12px;
  display: flex; align-items: center; gap: 14px;
  border-radius: 20px; overflow: hidden; isolation: isolate;
  color: var(--color-text-heading);
  background: rgb(24 24 27 / .86);
  -webkit-backdrop-filter: blur(24px) saturate(1.3); backdrop-filter: blur(24px) saturate(1.3);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .1), inset 0 1px 0 rgb(255 255 255 / .08), 0 24px 60px rgb(0 0 0 / .6);
  transform: translate(-50%, 24px); opacity: 0; visibility: hidden;
  transition: transform .4s var(--lc-ease), opacity .3s ease, visibility 0s linear .4s;
}
.mini-player.is-open { transform: translate(-50%, 0); opacity: 1; visibility: visible; transition-delay: 0s; }
.mini-player::before {                                       /* the cover's glow, washed in from the left */
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: radial-gradient(60% 140% at 0% 50%, rgb(var(--glow, 120 113 108) / .32), transparent 70%);
}
.mini-art {
  width: 44px; height: 44px; flex: none; border-radius: 7px; object-fit: cover; display: block;
  background: rgb(255 255 255 / .06);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .1), 0 4px 12px rgb(0 0 0 / .4);
}
.mini-text { min-width: 0; flex: 1; }
.mini-title { margin: 0; font: 400 17px/21px var(--font-heading); color: var(--color-text-heading); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mini-sub { margin: 2px 0 0; font: 500 14px/18px var(--font-body); color: var(--color-text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mini-disc {                                                 /* two identical 40px discs: play/pause (teal), close */
  position: relative; width: 40px; height: 40px; flex: none; padding: 0; border: 0; border-radius: 50%;
  display: grid; place-items: center; cursor: pointer;
  color: var(--color-text-heading); background: rgb(255 255 255 / .08); box-shadow: inset 0 0 0 1px rgb(255 255 255 / .14);
  -webkit-tap-highlight-color: transparent; transition: background-color .15s ease, transform .09s ease;
}
.mini-disc[hidden] { display: none; }
.mini-disc::before { content: ""; position: absolute; inset: -2px; border-radius: 50%; }   /* 44px target */
.mini-disc svg { width: 16px; height: 16px; grid-area: 1 / 1; }
.mini-disc.is-primary { background: var(--color-primary); color: #fff; box-shadow: inset 0 1px 0 rgb(255 255 255 / .2); }
.mini-disc:active { transform: scale(.94); }
.mini-disc:focus-visible { outline: 2px solid var(--lc-ring); outline-offset: 2px; }
@media (hover: hover) and (pointer: fine) {
  .mini-disc:hover { background: rgb(255 255 255 / .14); }
  .mini-disc.is-primary:hover { background: var(--color-primary-dark, #0F766E); }
}
/* One glyph per state: pause while loading / playing, play when paused, retry after an error. */
.mini-disc .mp-i { display: none; }
.mini-player:is([data-state="loading"], [data-state="playing"]) .mp-i--pause,
.mini-player[data-state="paused"] .mp-i--play,
.mini-player[data-state="error"] .mp-i--retry { display: block; }
.mini-player[data-state="paused"] .mp-i--play { margin-left: 2px; }
/* Loading / buffering: a thin arc turns around the pause glyph (the disc still pauses). */
.mini-player:is([data-state="loading"], [data-busy="true"]) .mini-disc.is-primary::after {
  content: ""; position: absolute; inset: 4px; border-radius: 50%; pointer-events: none;
  border: 2px solid rgb(255 255 255 / .28); border-top-color: #fff;
  animation: mp-spin .9s linear infinite;
}
@keyframes mp-spin { to { transform: rotate(360deg); } }
.mini-progress { position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: rgb(255 255 255 / .08); pointer-events: none; }
.mini-progress i {
  display: block; height: 100%; width: 100%; background: rgb(245 245 244 / .85);
  transform: scaleX(var(--mp-p, 0)); transform-origin: left; transition: transform .3s linear;
}
/* Room under the last row so the open bar never covers the end of the page. */
html.mini-open body::after { content: ""; display: block; height: 100px; }

@media (prefers-reduced-motion: reduce) {
  .card-listen { transform: none; transition: opacity .12s, visibility 0s linear .12s, background-color .12s; }
  .novel-card .card-more[data-owner-menu]::before { transform: none; transition: opacity .12s, background-color .12s; }
  .has-library-player .novel-card .card-listen:active { transform: none; }
  .mini-player { transition: opacity .12s, visibility 0s linear .12s; transform: translate(-50%, 0); }
  .mini-player.is-open { transition: opacity .12s, visibility 0s; }
  .mini-progress i, .mini-disc { transition: none; }
  .mini-disc:active { transform: none; }
  .mini-player:is([data-state="loading"], [data-busy="true"]) .mini-disc.is-primary::after { animation: none; border-color: rgb(255 255 255 / .5); }
}
