/* ==========================================================================
   Tworks — page-specific layout: home, catalog, book-detail, bookshelf
   ========================================================================== */

main { display: block; padding-top: var(--space-6); padding-bottom: var(--space-7); }

/* ---------- Home ---------- */

.dash-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-3); margin: var(--space-5) 0; }
.dash-stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--space-4); }
.dash-stat b { display: block; font-size: 23px; font-weight: 700; font-variant-numeric: tabular-nums; }
.dash-stat span { font-size: 12px; color: var(--text-faint); }
.dash-stat.warn b { color: var(--warning); }
.dash-stat.good b { color: var(--success); }

.dash-cols { display: grid; grid-template-columns: 1.6fr 1fr; gap: var(--space-5); align-items: start; }
.dash-panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--space-4); }
.dash-panel h3 { font-size: 14px; margin: 0 0 3px; }
.dash-panel .sub { font-size: 12px; color: var(--text-faint); margin-bottom: var(--space-3); }
.dash-attn-row { display: flex; align-items: center; gap: var(--space-3); padding: 10px 0; border-top: 1px solid var(--border); cursor: pointer; }
.dash-attn-row:first-of-type { border-top: none; }
.dash-attn-row .cover { width: 28px; flex: 0 0 28px; border-radius: 4px; }
.dash-attn-row .da-meta { flex: 1; min-width: 0; }
.dash-attn-row .da-meta .t { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 3px; }

/* Status chips were fully-rounded pills (var(--radius-pill) via the base
   .chip class in components.css); the "needs attention" list uses the
   slightly-rounded capsule shape instead. Scoped to these 4 status
   variants specifically — the pill shape is still correct for genre
   chips and other .chip uses elsewhere. */
.chip--not-sent, .chip--pending-conversion, .chip--partially-converted, .chip--fully-converted { border-radius: var(--radius-sm); }

.dash-quick { display: flex; flex-direction: column; gap: 8px; }
.dash-quick a { display: flex; align-items: center; gap: 9px; padding: 11px 12px; border-radius: var(--radius-md); border: 1px solid var(--border); color: var(--text); font-size: 13px; font-weight: 600; text-decoration: none; transition: background .15s ease, border-color .15s ease; }
.dash-quick a:hover { background: var(--surface-alt); border-color: var(--border-strong); }
.dash-quick a .ic { font-size: 16px; }
.dash-quick a .arrow { margin-left: auto; color: var(--text-faint); }

@media (max-width: 760px) {
  .dash-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* minmax(0, 1fr), not a bare 1fr — a bare Nfr track's implicit auto
     minimum is the intrinsic (min-content) width of its widest item, so a
     long unbroken novel title in .dash-panel forced this single column
     wider than the viewport instead of letting the item's own
     white-space:nowrap/ellipsis truncation (line 24) do its job, causing
     a page-wide horizontal scrollbar on mobile. */
  .dash-cols { grid-template-columns: minmax(0, 1fr); }
}

.continue-banner {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  background: linear-gradient(120deg, var(--brand-soft), #fff 65%);
  border: 1px solid var(--brand-soft-border);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
}
.continue-banner .cover { width: 64px; flex: 0 0 64px; }
.continue-banner .cb-meta { flex: 1; min-width: 0; }
.continue-banner .cb-eyebrow { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--brand-darker); }
.continue-banner h3 { font-size: 16px; margin: 3px 0 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.continue-banner .cb-progress { display: flex; align-items: center; gap: 10px; }
.continue-banner .cb-progress .progress-track { width: 140px; }
.continue-banner .cb-progress span { font-size: 11.5px; color: var(--text-soft); font-weight: 600; flex-shrink: 0; }
.continue-banner .btn { flex-shrink: 0; }

@media (max-width: 900px) {
  .continue-banner { flex-wrap: wrap; }
}
@media (max-width: 560px) {
  .continue-banner .cb-progress .progress-track { width: 90px; }
}

/* ---------- Catalog ---------- */

.catalog-layout { display: flex; flex-direction: column; gap: var(--space-5); }
.rank-tabs-panel { display: none; }
.rank-tabs-panel.active { display: block; }
.rank-tab-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--space-3) var(--space-6); }

.result-count { font-size: 13px; color: var(--text-faint); }

/* ---------- Book detail ---------- */

/* Shared width cap for every top-level block on Novel Detail. Each of
   .nd-hero/.synopsis-block/.nd-mobile-genre-row/.reading-view used to carry
   its own `max-width: 760px; margin: ... auto;` — a manual re-add after the
   old .novel-tab-content wrapper (which capped all of them at once) was
   removed in f78fb49. .nd-supplemental-links was added later and missed
   that re-add entirely, so it rendered at the page's full up-to-1240px
   .container width, flush against the left edge instead of centered in this
   column (fix NG, 2026-08-02). One class now, added alongside each block's
   own class in app.js; each block keeps only its own vertical margin. */
.nd-col-cap { max-width: 760px; margin-left: auto; margin-right: auto; }

.detail-header {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: var(--space-6);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
}
/* Mobile hero layout (<=640px) lives further down, in the "Novel Detail —
   mobile hero reskin" block — it replaces .detail-header's grid with the
   mock's side-by-side flex + blurred-backdrop treatment entirely, rather
   than just narrowing this grid's columns. */
.detail-meta { min-width: 0; display: flex; flex-direction: column; }
.detail-meta h1 { font-size: clamp(22px, 3vw, 30px); margin-bottom: 4px; }
.detail-meta .author-line { font-size: 14px; color: var(--text-soft); margin-bottom: var(--space-3); }
.detail-meta .tag-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: var(--space-3); }
.detail-meta .stat-row { display: flex; flex-wrap: wrap; gap: var(--space-5); margin-bottom: var(--space-4); font-size: 13px; }
.detail-meta .stat-row .stat b { display: block; font-size: 17px; }
.detail-meta .stat-row .stat span { color: var(--text-faint); font-size: 11.5px; text-transform: uppercase; letter-spacing: .04em; }
.detail-meta .cta-row { display: flex; gap: 10px; flex-wrap: wrap; margin-top: auto; }
.detail-meta .progress-note { font-size: 12.5px; color: var(--text-soft); margin-bottom: var(--space-3); display: flex; align-items: center; gap: 8px; }
.detail-meta .progress-note .progress-track { width: 160px; }

/* Longhand on purpose (Fix NG, 2026-08-02) — the 2-value shorthand this
   used to be (`margin: var(--space-6) 0`) explicitly zeroes the horizontal
   margin, which wins over .nd-col-cap's `margin-left/right: auto` at equal
   specificity since this rule comes later in the file, silently un-
   centering this block relative to every sibling that only sets
   margin-top/bottom (see .nd-supplemental-links below for the pattern this
   should have followed from the start). */
.synopsis-block { margin-top: var(--space-6); margin-bottom: var(--space-6); }
.synopsis-block p { color: var(--text-soft); line-height: 1.7; font-size: 14.5px; max-width: 74ch; }

.chapter-list-head {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  /* No margin-bottom here — .reading-view's own gap (var(--space-3)) already
     provides the space below this flex item; flex `gap` and a child's own
     margin both apply in full (flex items never collapse margins), so
     adding one here would double up the spacing. */
}
.chapter-search { position: relative; flex: 1; min-width: 200px; max-width: 320px; }
.chapter-search input {
  width: 100%; height: 38px; border-radius: var(--radius-md); border: 1px solid var(--border);
  background: var(--surface-alt); padding: 0 12px 0 34px; font-size: 13px;
}
.chapter-search svg { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); width: 15px; height: 15px; color: var(--text-faint); }

.chapter-table { border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; background: var(--surface); }
.chapter-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  font-size: 13.5px;
}
.chapter-row:last-child { border-bottom: none; }
.chapter-row:hover { background: var(--surface-alt); }

/* Tworks addition — Novel Detail's TOC rows are real <button>s (not a link)
   for data-chapter-index click handling; this resets button chrome so it
   lays out identically to the mock's plain-div .chapter-row content. */
.chapter-row .toc-open {
  all: unset;
  display: flex; align-items: center; gap: 12px;
  flex: 1; min-width: 0;
  cursor: pointer;
}
.chapter-row .toc-open .title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.chapter-row.read .toc-open .title { color: var(--text-faint); font-weight: 500; }
.toc-snippet { font-size: 11.5px; color: var(--text-faint); font-weight: 400; white-space: normal; }
.chapter-row .title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.chapter-row.read .title { color: var(--text-faint); font-weight: 500; }
.chapter-row .chapter-tags { display: flex; gap: 6px; flex-shrink: 0; }
/* Conversion-status chip — the row's only always-visible tag now (the
   index number is gone; read/unread status stays in .chapter-tags,
   .nd-desktop-only). Sits after .title in source order, so .title's own
   flex:1 naturally pushes it to the row's right edge without any explicit
   alignment rule; flex-shrink:0 keeps it full-size so .title's own
   ellipsis absorbs the squeeze on a long title instead. */
.chapter-row .toc-conversion-chip { flex-shrink: 0; }
/* Icon only, no label text, no chip background/border — chipHtml() still
   renders all three (the "always icon + text, never color alone" chip
   convention, styles.css, is correct for every OTHER place chipHtml() is
   used), so they're stripped here rather than changed at the source:
   font-size:0 collapses the chip's own text node to invisible/zero-width,
   and the icon (a ::before pseudo-element, styles.css's .chip--*::before)
   gets its own explicit font-size since it would otherwise inherit the
   same 0. gap:0 removes the now-pointless icon-to-text gap the base
   .chip rule reserves. background/border:none here (4 classes deep) also
   wins over .chip--fully-converted's own background override, which is
   more specific than the plain base .chip rule alone but not this. */
.reading-view .chapter-row .toc-conversion-chip .chip {
  padding: 0;
  font-size: 0;
  gap: 0;
  background: none;
  border: none;
}
.reading-view .chapter-row .toc-conversion-chip .chip::before { font-size: 16px; }

/* Novel Detail — this whole page (detail-header/nd-hero, synopsis, and the
   reading-view TOC below) now uses the same layout at every width,
   desktop/tablet included, matching the mock (output/tworks-ui-mock_ver2.0/
   novel-detail-mobile.html) unconditionally rather than only <=640px as in
   earlier rounds (FIX 1.18-1.26). `.nd-desktop-only` / `.nd-mobile-only`
   were the hide/show utilities driving that old split; kept as class names
   (still referenced from app.js) but simplified to their unconditional
   end-state now that there's no split left to drive. `.nd-desktop-only` is
   intentionally *not* just `.chapter-row .chapter-tags` etc. by ancestor
   selector — the Library page's dense table view (libraryTableHtml) reuses
   the same .chapter-row/.chapter-tags class names for an unrelated purpose,
   and an ancestor-based rule here would hide Library's own status chips
   too. !important is deliberate on .nd-desktop-only: it's a hide utility
   meant to win over whatever contextual, possibly-more-specific selector
   (e.g. `.detail-meta .tag-row`, 2 classes) styles the element it's stuck
   on — chasing per-element specificity instead is exactly the trap that
   broke this page's mobile layout twice before (see the FIX 1.18
   CSS-ordering note further down). */
.nd-desktop-only { display: none !important; }
/* .nd-mobile-only sets no display value of its own — every element it's
   used on also carries a second class (.nd-sort-icon-btn, .nd-section-
   title, .nd-synopsis-toggle, ...) that already declares the right display
   type for that element (inline-flex for an icon button, flex for a
   heading row, etc.); forcing one centrally here would fight that. */

/* margin-bottom only (Fix NG, 2026-08-02) — was the 3-value shorthand
   `margin: 0 0 var(--space-5)`, whose middle `0` zeroes the horizontal
   margin and wins over .nd-col-cap's `margin-left/right: auto`; see
   .synopsis-block above for the same bug and fuller explanation. */
.nd-mobile-genre-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: var(--space-5); }

.nd-sort-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; border-radius: 50%;
  border: 1px solid var(--border); background: var(--surface); color: var(--text-soft);
  flex-shrink: 0; margin-left: auto;
  transition: background .15s ease, color .15s ease;
}
.nd-sort-icon-btn svg { transition: transform .2s ease; }
.nd-sort-icon-btn[data-newest-first='true'] svg { transform: rotate(180deg); }

/* ---------- Novel Detail — hero reskin (all widths) ---------- */
/* Replaces .detail-header's desktop grid+card with a row hero over a
   blurred/scaled cover-as-backdrop, at every width now — cover on the left
   (plain `row`, source order already puts .cover before .detail-meta so no
   reordering needed), everything else (title/author/status/updated) on
   the right. Cover is square-cornered here (border-radius: 0 overrides
   components.css's base .cover radius) — a deliberate contrast against the
   rounded corners used everywhere else .cover appears on the site.
   Capped to the same 760px width as .reader-channel/.nd-bottombar
   elsewhere on this page/the Chapter Reading screen (via the shared
   .nd-col-cap class, added alongside .nd-hero in app.js) — without this,
   the hero's own text column stretches to fill whatever the page's outer
   .container width is (up to 1240px), which changes title/author wrapping
   and proportions enough that it stops actually looking like "the mobile
   version," even though the flex-row structure is identical.
   Background standardized to plain white (--surface) for every novel —
   `.nd-hero-backdrop` used to fill this area with a blurred/scaled/
   darkened copy of the novel's own cover image (see app.js renderNovel),
   which made every novel's hero a different color derived from its cover
   (or the shared default cover for coverless ones). Hidden rather than
   removed from the markup, since app.js still needs heroCoverSrc for the
   visible cover thumbnail next to it. */
.nd-hero {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  position: relative;
  padding: 20px;
  background: var(--surface);
  border: none;
  border-radius: 0;
  overflow: hidden;
}
.nd-hero-backdrop { display: none; }
.nd-hero .cover {
  position: relative;
  width: 180px; height: 240px; flex: 0 0 180px; margin: 0;
  border-radius: 0;
}
/* Reverted for mobile & tablet — back to the pre-180x240 size (108px
   wide, height falls out of the base .cover's own 3:4 aspect-ratio rather
   than a second explicit height override). Desktop (>1000px, same cutoff
   the header nav uses) keeps the 180x240 size above. */
@media (max-width: 1000px) {
  .nd-hero .cover { width: 108px; height: auto; flex: 0 0 108px; }
}
/* Text colors below were designed for the old dark blurred-cover backdrop
   (white text + dark text-shadow for contrast) — now that .nd-hero has a
   plain white background, that combination is invisible. Switched to the
   site's normal dark text tokens, same as everywhere else, no shadow. */
.nd-hero .detail-meta {
  position: relative;
  min-width: 0;
  color: var(--text);
}
.nd-hero .detail-meta h1 { text-shadow: none; }
.nd-hero .detail-meta .author-line { color: var(--text-soft); margin-bottom: 0; }
/* Plain inline flow (not flex) — the chip, separator, and timestamp are
   just inline content on one line, at every screen width. white-space:
   nowrap keeps them from ever breaking onto a second line; when the line
   is wider than the hero column, overflow/text-overflow truncate the
   whole line with a single trailing ellipsis rather than wrapping. */
.nd-hero-status-row {
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 10px;
  font-size: 12px;
  color: var(--text-soft);
  text-shadow: none;
}
.nd-hero-status-sep { display: inline; color: var(--text-faint); }
.nd-hero-updated { display: inline; }
/* Plain colored text, not a bordered/padded badge — no border, padding,
   or margin of its own (the .nd-hero-status-sep on either side already
   provides the visual gap). */
.nd-hero-status-row .chip {
  display: inline;
  border: none;
  border-radius: 0;
  padding: 0;
  margin: 0;
}

/* tworks-review 2026-08-02 P2 — the vote average has to be visible without
   opening the Vote modal (the modal is where the star picker itself lives).
   Own line rather than folded into .nd-hero-status-row: that row is
   white-space:nowrap with a single trailing ellipsis, so adding a rating
   segment there would risk truncating the existing status/updated-date text
   on narrow screens instead of the rating. */
.nd-hero-rating { margin-top: 6px; font-size: 12.5px; font-weight: 700; color: var(--gold); }
.nd-hero-rating .novel-vote-count { font-weight: 400; color: var(--text-faint); }

/* "Status of Novel" text color — novel.status (NOVEL_STATUS_LABELS),
   distinct from the chip--not-sent/chip--fully-converted family above
   which is the per-chapter *conversion* rollup, a different concept.
   Unified to a single color (all 4 statuses look the same, distinguished
   only by label text) rather than the old per-status background-tint set. */
.chip--novel-status-completed,
.chip--novel-status-ongoing,
.chip--novel-status-hiatus,
.chip--novel-status-unverified {
  background: transparent;
  color: #fdae31;
}

/* ---------- Novel Detail — synopsis clamp (all widths) ---------- */
/* Clamps to ~4 lines with an inline expand toggle, matching the mock, at
   every width now (previously desktop kept the synopsis unclamped).
   [Bug fix] Synopsis now renders as one real <p> per logical paragraph
   (paragraphBlocksHtml) instead of one pre-wrapped block, so the old
   per-<p> -webkit-line-clamp trick (which only reliably clamps a single
   text flow, not multiple block-level <p> children) is replaced with a
   max-height clamp on the wrapper — works the same regardless of how many
   paragraphs the synopsis actually has. 6.8em = 4 lines at this element's
   own 14.5px/1.7 font-size/line-height (set explicitly below rather than
   relying on .synopsis-block p's cascade, since the max-height needs to
   match the same metrics). */
.nd-section-title { font-size: 15px; font-weight: 800; }
.nd-synopsis { position: relative; }
.nd-synopsis-body {
  font-size: 14.5px;
  line-height: 1.7;
  overflow: hidden;
  overflow-wrap: break-word;
}
.nd-synopsis-body p { margin: 0 0 10.5px; }
.nd-synopsis-body p:last-child { margin-bottom: 0; }
.nd-synopsis:not(.is-expanded) .nd-synopsis-body { max-height: 6.8em; }
.nd-synopsis.is-expanded .nd-synopsis-body { max-height: none; }
.nd-synopsis-toggle {
  display: flex; align-items: center; gap: 3px;
  margin-top: 6px; background: none; border: none; padding: 0;
  font-size: 12.5px; font-weight: 700; color: var(--accent);
}
.nd-synopsis-toggle svg { width: 12px; height: 12px; transition: transform .2s ease; }
.nd-synopsis.is-expanded .nd-synopsis-toggle svg { transform: rotate(180deg); }

/* ---------- Novel Detail — TOC, 1:1 with the mock (all widths) ---------- */
/* Header: "Chapters" + inline stats (replacing the desktop-only bare
   result-count) plus the sort icon already added. Body: tightened row
   metrics to the mock's own numbers (gap 10 not 12, padding 12/14 not
   12/16, font 13 not 13.5). Scoped to `.reading-view .chapter-row` (not
   the bare `.chapter-row` these rules used at mobile widths before) —
   Library's dense table view (libraryTableHtml) reuses the same
   .chapter-row/.pagination class names for an unrelated table, and now
   that this isn't gated to <=640px anymore, an unscoped rule would
   restyle Library's rows at every width, not just leak in narrow ones.
   Footer: pagination adopts the mock's dashed "View all chapters" button
   look, but keeps real Prev/Page/Next controls rather than literally
   becoming a dead link — this page has no separate "full list"
   destination to link to (unlike the mock, which is a preview of
   book-detail.html), and a 2000+-chapter novel still needs pagination
   regardless of how the mock's own static demo looked. */
/* Same 760px cap as .nd-hero/.synopsis-block above, same reason: without
   it the TOC stretches to the page's full .container width on desktop
   (wide chapter-table rows, chapter-list-head controls spread out) instead
   of matching the mobile column's proportions. */

.nd-chapter-head-label { font-size: 15px; font-weight: 800; }
.reading-view .chapter-row { gap: 10px; padding: 12px 14px; font-size: 13px; }
.reading-view .pagination {
  flex-wrap: nowrap;
  overflow-x: auto;
  width: 100%;
  margin: 12px 0;
  padding: 8px 14px;
  border-radius: var(--radius-md);
  border: 1px dashed var(--border-strong);
  background: none;
}
.reading-view .pagination .btn { padding: 4px 10px; font-size: 12.5px; }
/* The pagination bar is rendered twice (above and below the chapter-table,
   tocBodyHtml in app.js) sharing the one .pagination rule above. The top
   copy's own margin-top would stack on top of .reading-view's gap (flex
   items never collapse margins), doubling the space above the table
   relative to below it — zeroed here so only the bottom copy's margin-top
   contributes a gap, matching the table-to-bottom-pagination spacing. */
#nd-toc-body > .toc-pagination:first-child { margin-top: 0; }

/* ---------- Novel Detail — sticky action bar (all widths) ---------- */
/* Stands in for .cta-row everywhere now (.cta-row is .nd-desktop-only, so
   permanently hidden) — its buttons proxy clicks to the (still-present,
   just hidden) desktop buttons/tabs rather than duplicating their logic,
   see app.js. Capped to the same 760px reading-column width the Chapter
   Reading screen's own .reader-channel uses and centered, rather than
   left:0/right:0 spanning the true edge-to-edge viewport width — at a wide
   desktop window a full-bleed fixed bar here would stretch 3 icons + one
   button across ~1200px with huge dead space between them, which doesn't
   "match the mobile version" so much as break it. */
.nd-bottom-spacer { display: block; height: 76px; }
.nd-bottombar {
  display: flex;
  align-items: center;
  gap: 10px;
  position: fixed;
  left: 0; right: 0; bottom: 0;
  max-width: 760px;
  margin: 0 auto;
  z-index: 40;
  padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
  background: rgba(255,255,255,0.96);
  backdrop-filter: blur(10px);
  border-top: 1px solid var(--border);
}

.nd-bottombar-icon {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  background: none; border: none; padding: 0; min-width: 44px;
  font-size: 9.5px; font-weight: 700; color: var(--text-faint);
}
.nd-bottombar-icon svg { width: 21px; height: 21px; }
.nd-bottombar-icon.is-active { color: var(--brand); }
.nd-bottombar-icon[aria-expanded='true'] { color: var(--brand); }

/* components.css hides `.nav-dropdown` at <=1000px (the header's own
   Account dropdown gives way to the hamburger drawer there) — override
   back to visible, since `.nd-edit-menu` only borrows that class for its
   open/close CSS (`.nav-dropdown[data-open='true'] .nav-dropdown-menu`),
   not the header's dropdown itself. */
.nd-edit-menu { display: inline-flex; position: relative; }
.nd-edit-menu .nav-dropdown-menu {
  top: auto; right: auto; bottom: calc(100% + 12px); left: 50%;
  min-width: 200px;
  transform: translateX(-50%) translateY(6px);
}
.nd-edit-menu[data-open='true'] .nav-dropdown-menu { transform: translateX(-50%) translateY(0); }
/* Hover submenu, mouse/trackpad only (touch devices keep the tap-to-toggle
   behavior above — data-open/click-driven, untouched). ::before bridges the
   12px gap between the trigger button and the menu (which opens upward,
   `bottom: calc(100% + 12px)` above): without it, moving the pointer from
   the button up into the menu crosses a dead zone with nothing under the
   cursor, so .nd-edit-menu:hover drops and the menu closes before it can be
   reached. The bridge is a descendant of .nav-dropdown-menu, so it only
   becomes hoverable once the menu itself is already visible — it can't be
   used to open the menu on its own, only to keep it open while crossing. */
@media (hover: hover) and (pointer: fine) {
  .nd-edit-menu:hover .nav-dropdown-menu {
    opacity: 1;
    visibility: visible;
    transform: translateX(-50%) translateY(0);
  }
  .nd-edit-menu .nav-dropdown-menu::before {
    content: '';
    position: absolute;
    left: 0; right: 0; bottom: -12px;
    height: 12px;
  }
}
.nd-edit-menu .nav-dropdown-menu button {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 10px 12px; border: none; background: none;
  border-radius: var(--radius-sm); font-family: var(--font-ui);
  font-size: 13px; font-weight: 600; color: var(--text); text-align: left;
}
.nd-edit-menu .nav-dropdown-menu button:hover { background: var(--surface-alt); }
.nd-edit-menu .nav-dropdown-menu button:disabled { color: var(--text-faint); cursor: not-allowed; }
.nd-edit-menu .nav-dropdown-menu button:disabled:hover { background: none; }
.nd-edit-menu .nav-dropdown-menu button svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--text-faint); }
.nd-edit-menu .nav-dropdown-menu button.is-danger { color: var(--danger); }
.nd-edit-menu .nav-dropdown-menu button.is-danger svg { color: var(--danger); }

.nd-bottombar-cta {
  flex: 1;
  display: flex; align-items: center; justify-content: center;
  height: 46px;
  border-radius: var(--radius-pill);
  background: var(--brand);
  color: #fff;
  font-size: 14.5px;
  font-weight: 800;
  border: none;
  box-shadow: 0 6px 16px rgba(217,48,37,0.35);
}

/* ---------- Bookshelf ---------- */

.shelf-section { margin-bottom: var(--space-7); }
.shelf-history-row {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--border);
}
.shelf-history-row .cover { width: 46px; flex: 0 0 46px; }
.shelf-history-row .h-meta { flex: 1; min-width: 0; }
.shelf-history-row .h-meta .title { font-weight: 700; font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.shelf-history-row .h-meta .sub { font-size: 12px; color: var(--text-faint); }

/* Novel-wide edit history 窶・approved 2026-07-29. */
.novel-history-card { width: min(680px, calc(100vw - 28px)); max-height: min(760px, 90vh); overflow: auto; }
.novel-history-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding-bottom: 14px; border-bottom: 1px solid var(--border); }
.novel-history-head h3 { margin: 0; }
.novel-history-head p { margin: 3px 0 0; color: var(--text-faint); font-size: 12px; }
.novel-history-entry { display: grid; grid-template-columns: 138px minmax(0, 1fr); gap: 16px; padding: 17px 0; border-bottom: 1px dashed var(--border); }
.novel-history-meta { font-size: 12px; color: var(--text-faint); }
.novel-history-meta strong, .novel-history-meta time { display: block; }
.novel-history-meta strong { color: var(--text); margin-bottom: 4px; }
.novel-history-changes section + section { margin-top: 14px; }
.novel-history-changes section > strong { display: block; margin-bottom: 6px; font-size: 13px; }
.novel-history-diff { display: grid; grid-template-columns: minmax(0, 1fr) 18px minmax(0, 1fr); align-items: center; gap: 6px; }
.novel-history-diff > div { padding: 9px; border-radius: var(--radius-sm); background: var(--surface-sunken); overflow-wrap: anywhere; white-space: pre-wrap; font-size: 12px; }
.novel-history-diff > div:last-child { background: var(--success-soft, #edf7ee); }
.novel-history-diff > span { text-align: center; color: var(--text-faint); }
.history-empty-value, .novel-history-empty { color: var(--text-faint); font-style: italic; }
.novel-history-error { text-align: center; padding: 28px 0; color: var(--danger); }
.novel-history-pagination { display: flex; align-items: center; justify-content: center; gap: 5px; padding-top: 18px; }
.novel-history-page { min-width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); }
.novel-history-page.active { color: #fff; background: var(--brand); border-color: var(--brand); }
.novel-history-page:disabled { opacity: .35; cursor: not-allowed; }
@media (max-width: 560px) {
  .novel-history-entry { grid-template-columns: 1fr; gap: 8px; }
  .novel-history-diff { grid-template-columns: 1fr; }
  .novel-history-diff > span { transform: rotate(90deg); }
  .novel-history-pagination { gap: 3px; }
  .novel-history-page { min-width: 29px; }
}

/* Novel history follow-up: compact changed-field summaries and native details. */
.novel-history-head .modal-close-x { margin-top: -4px; }
.novel-history-entry { grid-template-columns: 124px minmax(0, 1fr); gap: 20px; }
.novel-history-changes { display: grid; gap: 9px; min-width: 0; }
.novel-history-change summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 3px 0;
  cursor: pointer;
  list-style: none;
}
.novel-history-change summary::-webkit-details-marker { display: none; }
.novel-history-change summary > strong { min-width: 0; font-size: 13px; }
.novel-history-more { flex: 0 0 auto; color: var(--brand); font-size: 12px; text-decoration: underline; text-underline-offset: 2px; }
.novel-history-more-close { display: none; }
.novel-history-change[open] .novel-history-more-open { display: none; }
.novel-history-change[open] .novel-history-more-close { display: inline; }
.novel-history-diff { align-items: start; margin-top: 9px; }
.novel-history-diff > .novel-history-value { padding: 0; border-radius: 0; background: none; white-space: normal; font-size: inherit; }
.novel-history-diff > .novel-history-value:last-child { background: none; }
.novel-history-value > span {
  display: block;
  margin-bottom: 4px;
  color: var(--text-faint);
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
}
.novel-history-value > div { padding: 9px; border-radius: var(--radius-sm); background: var(--surface-sunken); overflow-wrap: anywhere; white-space: pre-wrap; font-size: 12px; }
.novel-history-value:last-child > div { background: var(--success-soft, #edf7ee); }
.novel-history-arrow { align-self: center; text-align: center; color: var(--text-faint); }
@media (max-width: 560px) {
  .novel-history-entry { grid-template-columns: 1fr; gap: 8px; }
  .novel-history-arrow { transform: rotate(90deg); }
}
