/* ============================================================
   Design tokens — three themes from two reference palettes.

   Light and dark come from the cool violet reference: #532aa8 primary on
   blue-whites, tight radii (4/8/12), flat surfaces, purple-tinted shadows.
   Sparkle comes from its own magenta reference and is deliberately a
   different *shape* as well as a different colour: radii jump to 16/32/48
   and the book card becomes translucent glass over a pink wash.

   That is why the radius, card and switch tokens live inside the theme
   blocks rather than in one shared root — a theme here changes form, not
   just hue, and every component reads the same token names in all three.
   ============================================================ */
:root {
  /* Colour roles */
  --md-primary: #532aa8;
  --md-on-primary: #ffffff;
  /* Note the inversion, straight from the reference: primary-container is a
     deep purple carrying pale text, so a filled state reads as a bold chip
     rather than a tint. Every active control in the app leans on that. */
  --md-primary-container: #6b46c1;
  --md-on-primary-container: #e1d2ff;
  --md-secondary: #6a548a;
  --md-secondary-container: #dbc1ff;
  --md-on-secondary-container: #614c81;
  --md-tertiary-container: #e5e1ea;
  --md-on-tertiary-container: #1c1b22;
  --md-background: #f9f9ff;
  --md-surface: #f9f9ff;
  --md-surface-container-low: #f0f3ff;
  --md-surface-container: #e7eeff;
  --md-surface-container-high: #dee8ff;
  --md-surface-variant: #d9e3f9;
  --tbr-panel: #ffffff;
  /* Generated fallback cover: cool and pale, to sit with the blue-whites. */
  --fc-s: 46%;
  --fc-l: 84%;
  --fc-ink: #1f1830;
  --fc-rule: rgba(0,0,0,.14);
  --md-on-surface: #121c2c;
  --md-on-surface-variant: #494453;
  /* `outline` draws a boundary that has to be seen (checkbox, cover choice);
     `outline-variant` is the reference's hairline, used for dividers and for
     tracks whose state is carried by a fill rather than by the border. */
  --md-outline: #7a7484;
  --md-outline-variant: #cbc3d5;
  --md-error: #ba1a1a;
  --md-on-error: #ffffff;
  --md-error-container: #ffdad6;
  --md-on-error-container: #93000a;

  --md-disabled-bg: rgba(18,28,44,.12);
  --md-disabled-fg: rgba(18,28,44,.38);
  --md-inverse-surface: #273141;
  --md-inverse-on-surface: #ebf1ff;

  /* Shape */
  /* --r-xl is every large surface: cards, the modal, the dropdown panels,
     cover art. It was tightened a step because the panels clip their
     children (`overflow: hidden`) and the wider curve bit into the first
     and last rows of the tag and shelf pickers. It currently resolves the
     same as --r-lg; the two names are kept apart so a component still says
     which it means, and large surfaces can be retuned on their own. */
  --r-sm: 4px;
  --r-lg: 8px;
  --r-xl: 8px;
  --r-full: 9999px;

  /* Elevation — tinted with the primary, so shadows read as part of the
     palette instead of as grey smudges over a coloured ground. */
  --md-elev-1: 0 2px 8px rgba(107,70,193,.10);
  --md-elev-2: 0 4px 12px rgba(107,70,193,.18);
  --md-elev-3: 0 8px 24px rgba(107,70,193,.18), 0 2px 8px rgba(18,28,44,.10);
  --nav-shadow: 4px 0 24px rgba(107,70,193,.03);

  /* Book card. Light and dark follow the reference: the card itself is not a
     surface at all — the cover floats on the page and carries the shadow. */
  --card-bg: transparent;
  --card-border: transparent;
  --card-blur: none;
  --card-shadow: none;
  --card-shadow-hover: none;
  --card-pad: 0px;
  --card-hover-lift: translateY(-4px);
  --cover-shadow: var(--md-elev-2);
  --cover-shadow-hover: var(--md-elev-3);

  /* Segmented controls: which pair fills the selected segment. */
  --switch-active-bg: var(--md-primary-container);
  --switch-active-fg: var(--md-on-primary-container);

  --body-wash: none;

  /* Release panel: violet-tinted, so it separates from the blue-white surface
     ladder rather than reading as one more card. */
  --rel-panel-bg: #f3ecff;
  --rel-panel-border: #d9c7ff;

  /* Skeleton shimmer. Near-white on a pale placeholder: the sheen has to be
     lighter than the block it crosses, which is a different colour in each
     theme, so this cannot be one shared constant. */
  --sk-sweep: rgba(255,255,255,.75);

  color-scheme: light;
}

/* ---------- Dark ----------
   Its own reference, and unlike the other two it is neutral: the surfaces are
   greys climbing #0e0e0e -> #353534 rather than anything tinted, and all the
   colour sits in the accents — lavender primary, a teal secondary, a rose
   tertiary. Note that primary-container inverts relative to light: here it is
   the *pale* lavender carrying dark text, which is what makes a selected nav
   row or filter pill read as a bright chip against the near-black.

   Radii and type are unchanged: shape belongs to the app, not to the theme
   toggle, and flipping to dark should not resize or re-weight any text. */
:root[data-theme="dark"] {
  --md-primary: #dab9ff;
  --md-on-primary: #460283;
  --md-primary-container: #bb86fc;
  --md-on-primary-container: #4c0f89;
  --md-secondary: #46f5e0;
  --md-secondary-container: #00d8c4;
  --md-on-secondary-container: #005950;
  --md-tertiary-container: #ec7d90;
  --md-on-tertiary-container: #68162b;
  --md-background: #131313;
  --md-surface: #131313;
  --md-surface-container-low: #1c1b1b;
  --md-surface-container: #201f1f;
  --md-surface-container-high: #2a2a2a;
  --md-surface-variant: #353534;
  --tbr-panel: #353534;
  /* Neutral, but a clear step above the ground: on #131313 a shadow does
     nothing, so the generated cover has to separate itself by being lighter
     than the page — the same way the surface ladder carries depth here. */
  --fc-s: 22%;
  --fc-l: 31%;
  --fc-ink: #e5e2e1;
  --fc-rule: rgba(255,255,255,.16);
  --md-on-surface: #e5e2e1;
  --md-on-surface-variant: #cdc3d4;
  /* Same split as light: `outline` is the boundary that has to be seen (5.9:1
     here), `outline-variant` the hairline that only has to be sensed. */
  --md-outline: #978d9d;
  --md-outline-variant: #4b4452;
  --md-error: #ffb4ab;
  --md-on-error: #690005;
  --md-error-container: #93000a;
  --md-on-error-container: #ffdad6;

  --md-disabled-bg: rgba(229,226,225,.12);
  --md-disabled-fg: rgba(229,226,225,.38);
  --md-inverse-surface: #e5e2e1;
  --md-inverse-on-surface: #313030;

  /* The reference carries the light theme's purple-tinted shadows over
     unchanged, but an 8%-alpha tint is invisible on a near-black ground. Depth
     here comes from the surface ladder above; these stay black with more
     spread so a raised element still has an edge. */
  --md-elev-1: 0 2px 8px rgba(0,0,0,.50);
  --md-elev-2: 0 4px 12px rgba(0,0,0,.58);
  --md-elev-3: 0 8px 24px rgba(0,0,0,.65), 0 2px 8px rgba(0,0,0,.50);
  --nav-shadow: 4px 0 24px rgba(0,0,0,.35);

  /* The one tinted surface in an otherwise neutral theme — it has to lift off
     a grey ladder, and a grey cannot do that against greys. */
  --rel-panel-bg: #221c2b;
  --rel-panel-border: #3b3148;

  /* Barely there: on a #1c1c1c block anything stronger reads as a flash. */
  --sk-sweep: rgba(255,255,255,.07);

  color-scheme: dark;
}

/* ---------- Sparkle ----------
   Its own reference, and a deliberately different shape language: radii jump
   from 8px to 32px and the book card stops being invisible and becomes a pane
   of translucent glass over a pink wash. */
:root[data-theme="sparkle"] {
  --md-primary: #89004b;
  --md-on-primary: #ffffff;
  --md-primary-container: #e10080;
  /* The reference pairs #ffc5d7 with this, which measures 3.15:1 — fine for
     the large button it uses there, not for the 12px chips this app puts on
     it. White is what the reference's own filled button uses, and it clears
     4.6:1, so that is the pairing kept here. */
  --md-on-primary-container: #ffffff;
  --md-secondary: #a120a3;
  --md-secondary-container: #f2b5f2;
  --md-on-secondary-container: #77007a;
  --md-tertiary-container: #9af892;
  --md-on-tertiary-container: #002203;
  --md-background: #fff8f8;
  --md-surface: #fff8f9;
  --md-surface-container-low: #fff0f3;
  --md-surface-container: #ffe8ed;
  --md-surface-container-high: #fce2e8;
  --md-surface-variant: #f6dce2;
  --tbr-panel: #ffffff;
  --fc-s: 62%;
  --fc-l: 88%;
  --fc-ink: #3a1030;
  --fc-rule: rgba(0,0,0,.14);
  --md-on-surface: #26181c;
  --md-on-surface-variant: #584048;
  --md-outline: #8c7078;
  --md-outline-variant: #dfbec7;
  --md-error: #ba1a1a;
  --md-on-error: #ffffff;
  --md-error-container: #ffdad6;
  --md-on-error-container: #93000a;

  --md-disabled-bg: rgba(38,24,28,.12);
  --md-disabled-fg: rgba(38,24,28,.38);
  --md-inverse-surface: #3c2c31;
  --md-inverse-on-surface: #ffecf0;

  /* Round, not tight — the single biggest reason sparkle reads as a different
     app rather than a recolour of the same one. */
  --r-sm: 16px;
  --r-lg: 32px;
  /* Tightened with the light/dark scale. Sparkle stays much rounder than
     the other two - 32px against 8px is still the shape that makes it read
     as a different app - but 48px was clipping the picker rows outright. */
  --r-xl: 32px;
  --r-full: 9999px;

  --md-elev-1: 0 2px 10px rgba(225,0,128,.10);
  --md-elev-2: 0 4px 20px rgba(225,0,128,.12);
  --md-elev-3: 0 12px 30px rgba(161,32,163,.20), 0 2px 8px rgba(38,24,28,.10);
  --nav-shadow: 4px 0 30px rgba(255,20,147,.10);

  /* Glass: the card becomes a real (translucent) surface here, so it takes the
     shadow and the cover inside it goes flat. */
  --card-bg: rgba(255,255,255,.7);
  --card-border: rgba(255,255,255,.8);
  --card-blur: blur(12px);
  --card-shadow: 0 4px 20px rgba(225,0,128,.05);
  --card-shadow-hover: 0 12px 30px rgba(161,32,163,.15);
  --card-pad: 8px;
  --card-hover-lift: translateY(-4px) scale(1.02);
  --cover-shadow: none;
  --cover-shadow-hover: none;

  --switch-active-bg: var(--md-secondary-container);
  --switch-active-fg: var(--md-on-secondary-container);

  /* Two soft washes anchored to opposite corners, fixed so they stay put while
     the grid scrolls past them. */
  --body-wash:
    radial-gradient(circle at top right, rgba(255,217,227,.4) 0%, transparent 40%),
    radial-gradient(circle at bottom left, rgba(255,215,247,.3) 0%, transparent 40%);

  /* Translucent like everything else here: the wash shows through it. */
  --rel-panel-bg: rgba(255,221,242,.55);
  --rel-panel-border: rgba(225,0,128,.18);

  --sk-sweep: rgba(255,255,255,.8);

  color-scheme: light;
}

/* The brand is the one place sparkle spends its gradient, rather than washing
   the whole panel in it as the old theme did. */
:root[data-theme="sparkle"] .brand {
  background: linear-gradient(135deg, #e10080 0%, #a120a3 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
:root[data-theme="sparkle"] .brand em,
:root[data-theme="sparkle"] .brand .material-symbols-outlined { color: transparent; }
/* Frosted rather than solid, matching the card treatment. */
:root[data-theme="sparkle"] .sidenav,
:root[data-theme="sparkle"] .topbar {
  background: rgba(255,255,255,.72);
  backdrop-filter: blur(14px);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: "Be Vietnam Pro", "Segoe UI", system-ui, sans-serif;
  background-color: var(--md-background);
  /* `none` in light and dark; two radial washes in sparkle. Fixed so they stay
     anchored to the viewport corners while the grid scrolls past. */
  background-image: var(--body-wash);
  background-attachment: fixed;
  color: var(--md-on-surface);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--md-primary); text-decoration: none; }
a:hover { text-decoration: underline; }

.material-symbols-outlined {
  font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 24;
  font-size: 20px;
  line-height: 1;
  vertical-align: middle;
  user-select: none;
}

/* ---------- Side navigation ----------
   One panel, two behaviours. From --nav-bp up it is a fixed column down the
   left and the body is simply padded out of its way; below that it slides in
   over the page as a drawer and the mobile top bar carries the hamburger that
   opens it. Only CSS differs between the two — the markup is identical. */
:root { --nav-w: 256px; }

.sidenav {
  position: fixed;
  top: 0; left: 0; bottom: 0;
  z-index: 40;
  width: var(--nav-w);
  display: flex;
  flex-direction: column;
  padding: 24px 16px;
  background: var(--md-surface-container-low);
  border-right: 1px solid var(--md-outline-variant);
  box-shadow: var(--nav-shadow);
  color: var(--md-on-surface);
  overflow-y: auto;
}

.brand {
  font-size: 1.375rem;
  font-weight: 500;
  letter-spacing: 0;
  display: flex;
  align-items: center;
  gap: 8px;
}
.brand .material-symbols-outlined { color: var(--md-primary); font-size: 26px; }
.brand em { color: var(--md-primary); font-style: normal; font-weight: 500; }
/* The panel is narrower than the old top bar, so the brand no longer fits on
   one line. Break it deliberately after "Never Ending" and indent "TBR" under
   it, rather than leaving it to wrap wherever the font happens to run out. */
.sidenav .brand {
  padding: 8px 12px 20px;
  color: inherit;
  text-decoration: none;
  flex-wrap: wrap;
  font-size: 1.25rem;
  line-height: 1.15;
}
.sidenav .brand em { flex-basis: 100%; padding-left: 34px; }
.sidenav .brand:hover { text-decoration: none; }

/* flex:1 makes the nav claim the panel's spare height, which is what lets the
   account block sit at the bottom on its own margin-top:auto. */
.sidenav nav { display: flex; flex-direction: column; gap: 4px; flex: 1; }

.sidenav .nav-user {
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid var(--md-outline-variant);
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.sidenav .user {
  display: flex; align-items: center; gap: 8px;
  color: var(--md-on-surface-variant); font-size: .875rem;
  padding: 4px 16px;
}
/* flex-shrink and object-fit both matter: the row is a flex line, so a long
   display name squashes an unprotected image into an oval, and a Google
   photo is not reliably square. The .avatar disc below already had the
   first of these; the photo did not. */
.sidenav .user img {
  width: 32px; height: 32px; flex-shrink: 0;
  border-radius: 50%; object-fit: cover;
}
/* Stand-in for a missing Google photo: the reference's filled initial disc. */
.sidenav .user .avatar {
  width: 32px; height: 32px; flex-shrink: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--md-primary); color: var(--md-on-primary);
  font-size: 14px; font-weight: 600; letter-spacing: 0;
}
.sidenav .user span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Also margin-top:auto, so the theme picker stays pinned to the bottom on the
   signed-out page, where the nav above it is display:none. */
.sidenav-foot { margin-top: auto; padding-top: 12px; }
.sidenav-foot .theme-switch { display: flex; width: 100%; margin: 0; }
.sidenav-foot .theme-switch button { flex: 1; width: auto; }

/* Full-width MD3 navigation rows. Scoped to `nav` so the brand link above it
   keeps its own spacing — the account block is inside the nav, so it is
   covered too. */
.sidenav nav a {
  display: flex; align-items: center; gap: 16px;
  padding: 12px 16px; border-radius: var(--r-xl);
  color: var(--md-on-surface-variant); font-size: 14px; font-weight: 600;
  letter-spacing: .03em;
  position: relative; overflow: hidden; text-decoration: none;
}
.sidenav nav a::before {
  content: ""; position: absolute; inset: 0;
  background: currentColor; opacity: 0; transition: opacity .15s ease;
}
.sidenav nav a:hover::before { opacity: .08; }
.sidenav nav a:active::before { opacity: .12; }
.sidenav a:hover { text-decoration: none; }
/* The row for the page you're on stays filled so the current section is obvious. */
.sidenav a.tab.active {
  background: var(--switch-active-bg);
  color: var(--switch-active-fg);
  box-shadow: var(--md-elev-1);
}

/* Grouping options nested under My Collection. Quieter than a top-level row
   in every respect — smaller, lighter, no icon, tinted rather than filled when
   selected — so the parent still reads as the section and these as choices
   within it. The rule down the left is what makes them look attached to it. */
.sidenav .subnav {
  display: flex; flex-direction: column; gap: 2px;
  margin: 4px 0 8px 22px;
  padding-left: 10px;
  border-left: 2px solid var(--md-outline-variant);
}
.sidenav .subnav a {
  padding: 8px 14px;
  font-size: 13px; font-weight: 600; letter-spacing: .02em;
  border-radius: var(--r-xl);
  color: var(--md-on-surface-variant);
}
.sidenav .subnav a[aria-current="page"] {
  background: var(--md-surface-container-high);
  color: var(--md-primary);
}

/* ---------- Mobile top bar ---------- */
.topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 0 8px;
  height: 64px;
  background: var(--md-surface-container-low);
  border-bottom: 1px solid var(--md-outline-variant);
  color: var(--md-on-surface);
}

.nav-scrim {
  position: fixed; inset: 0; z-index: 39;
  background: rgba(0, 0, 0, .4);
}
.nav-scrim[hidden] { display: none; }
/* Paired with the drawer: the page behind it must not scroll away underneath. */
body.nav-locked { overflow: hidden; }

/* ---------- Theme picker (MD3 segmented button) ----------
   Three side-by-side options — light / dark / sparkle. The selected one stays
   filled so the current theme is obvious at a glance. */
.theme-switch {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  background: var(--md-surface-container);
  border: 1px solid var(--md-outline-variant);
  border-radius: var(--r-full);
  margin-right: 4px;
}
.theme-switch button {
  width: 40px; height: 32px; min-height: 32px; padding: 0;
  border: none; border-radius: var(--r-full); cursor: pointer;
  background: transparent; color: var(--md-on-surface-variant);
  display: inline-flex; align-items: center; justify-content: center;
  position: relative; overflow: hidden;
  transition: background .15s ease, color .15s ease;
}
.theme-switch button:hover { box-shadow: none; }
.theme-switch button .material-symbols-outlined { font-size: 20px; }
.theme-switch button[aria-pressed="true"] {
  background: var(--switch-active-bg);
  color: var(--switch-active-fg);
  box-shadow: var(--md-elev-1);
}

@media (max-width: 560px) {
  .theme-switch button { width: 34px; }
  .theme-switch button .material-symbols-outlined { font-size: 18px; }
}

/* MD3 icon button */
.icon-btn {
  width: 40px; height: 40px; min-height: 40px; padding: 0;
  border-radius: 50%; border: none; cursor: pointer;
  background: transparent; color: var(--md-on-surface-variant);
  display: inline-flex; align-items: center; justify-content: center;
  position: relative; overflow: hidden;
}
.icon-btn:hover { box-shadow: none; }
.icon-btn .material-symbols-outlined { font-size: 22px; }
/* The panel is fixed, so the body is padded out of its way rather than the
   content being wrapped in a column: modals, the scrim and the sparkle layer
   are all position:fixed and keep covering the whole viewport. */
@media (min-width: 900px) {
  body { padding-left: var(--nav-w); }
  .topbar { display: none; }
  /* The collection's grouping pills only: the panel is on screen, so they
     would be a second copy of the same control. Scoped by id, because other
     pages put unrelated switches in the same slot. */
  #viewSwitch { display: none; }
}

@media (max-width: 899px) {
  /* The panel is behind the hamburger here, so grouping goes back above the
     grid where it is one tap rather than three. */
  .sidenav .subnav { display: none; }
  .sidenav {
    transform: translateX(-100%);
    transition: transform .22s ease;
    box-shadow: var(--md-elev-3);
  }
  .sidenav.open { transform: none; }
}

main { max-width: 880px; margin: 0 auto; padding: 24px 16px 48px; }

/* ---------- Typography ---------- */
/* Both references share one scale: a heavy, tightly-tracked headline over a
   16px body, with labels at 14px/600 and a touch of positive tracking. */
h1 { font-size: 32px; font-weight: 700; line-height: 1.2; letter-spacing: -.02em; margin: 8px 0 4px; }
h2 { font-size: 24px; font-weight: 600; line-height: 1.3; letter-spacing: -.01em; margin: 0 0 16px; }
h3 { font-size: 20px; font-weight: 600; line-height: 1.4; margin: 0 0 12px; }
.subtitle { color: var(--md-on-surface-variant); margin: 0 0 24px; font-size: 15px; }

@media (max-width: 560px) {
  h1 { font-size: 24px; letter-spacing: -.01em; }
  h2 { font-size: 20px; }
}

/* ---------- Cards (MD3 filled card) ---------- */
.card {
  background: var(--md-surface-container-low);
  border-radius: var(--r-xl);
  padding: 24px;
  margin-bottom: 20px;
}

/* ---------- Buttons ---------- */
button {
  font-family: inherit;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: .03em;
  border: none;
  border-radius: var(--r-full);
  padding: 10px 24px;
  min-height: 40px;
  cursor: pointer;
  background: var(--md-primary);
  color: var(--md-on-primary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  position: relative;
  overflow: hidden;
  transition: box-shadow .15s ease, background .15s ease;
}
button::before {
  content: ""; position: absolute; inset: 0;
  background: currentColor; opacity: 0; transition: opacity .15s ease;
}
button:hover { box-shadow: var(--md-elev-1); }
button:hover::before { opacity: .08; }
button:active::before { opacity: .12; }
button:disabled {
  background: var(--md-disabled-bg); color: var(--md-disabled-fg);
  cursor: not-allowed; box-shadow: none;
}
button:disabled::before { opacity: 0; }

/* Outlined button */
button.secondary {
  background: transparent;
  color: var(--md-primary);
  border: 1px solid var(--md-outline);
}
button.secondary:hover { box-shadow: none; }

/* Filled error button */
button.danger { background: var(--md-error); color: var(--md-on-error); }

/* ---------- Filled text fields (floating label) ---------- */
.field { position: relative; margin: 14px 0 0; }
.field input,
.field textarea,
.field select {
  width: 100%;
  font-family: inherit;
  font-size: 1rem;
  color: var(--md-on-surface);
  background: var(--md-surface-container-high);
  border: none;
  border-bottom: 1px solid var(--md-on-surface-variant);
  border-radius: var(--r-sm) var(--r-sm) 0 0;
  padding: 22px 16px 6px;
  transition: border-color .15s ease, background .15s ease;
}
.field textarea { resize: vertical; min-height: 88px; padding-top: 24px; }
/* The TBR reason is optional and usually short, so it opens at a single line
   rather than the summary box's height. Still drag-resizable for longer notes. */
#m_reason, #f_reason { min-height: 48px; }
.field select { appearance: none; cursor: pointer; }
.field::after { /* dropdown chevron for selects */
  pointer-events: none;
}
.field.field--select::after {
  content: "expand_more";
  font-family: "Material Symbols Outlined";
  position: absolute; right: 12px; top: 18px;
  color: var(--md-on-surface-variant);
}
.field input:hover,
.field textarea:hover,
.field select:hover { background: var(--md-surface-variant); }
.field input:focus,
.field textarea:focus,
.field select:focus {
  outline: none;
  border-bottom: 2px solid var(--md-primary);
  padding-bottom: 5px; /* compensate thicker border */
}

.field label {
  position: absolute;
  left: 16px;
  top: 18px;
  color: var(--md-on-surface-variant);
  font-size: 1rem;
  pointer-events: none;
  transition: top .15s ease, font-size .15s ease, color .15s ease;
}
.field input:focus + label,
.field input:not(:placeholder-shown) + label,
.field textarea:focus + label,
.field textarea:not(:placeholder-shown) + label,
.field select + label {
  top: 7px;
  font-size: .75rem;
}
.field input:focus + label,
.field textarea:focus + label,
.field select:focus + label {
  color: var(--md-primary);
}

/* ---------- File picker (filled tonal button) ---------- */
.file-btn {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: .875rem; font-weight: 500;
  background: var(--md-secondary-container);
  color: var(--md-on-secondary-container);
  padding: 10px 24px; min-height: 40px;
  border-radius: var(--r-full); cursor: pointer;
  position: relative; overflow: hidden;
  transition: box-shadow .15s ease;
}
.file-btn::before {
  content: ""; position: absolute; inset: 0;
  background: currentColor; opacity: 0; transition: opacity .15s ease;
}
.file-btn:hover { box-shadow: var(--md-elev-1); }
.file-btn:hover::before { opacity: .08; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

.preview-wrap { text-align: center; margin: 20px 0; }
.preview-wrap img {
  max-height: 320px; max-width: 100%;
  border-radius: var(--r-xl); box-shadow: var(--md-elev-2);
}

/* ---------- Mode switch (photo vs manual) ---------- */
.mode-switch {
  display: flex; gap: 2px; margin-bottom: 24px; padding: 4px;
  background: var(--md-surface-container);
  border: 1px solid var(--md-outline-variant);
  border-radius: var(--r-full);
}
.mode-switch .mode-btn {
  flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 10px 16px; border: 0; border-radius: var(--r-full); cursor: pointer;
  background: transparent; color: var(--md-on-surface-variant);
  font-size: 14px; font-weight: 600; letter-spacing: .03em; box-shadow: none;
  transition: background .2s ease, color .2s ease, box-shadow .2s ease;
}
.mode-switch .mode-btn:hover { background: var(--md-surface-variant); }
.mode-switch .mode-btn.active {
  background: var(--switch-active-bg);
  color: var(--switch-active-fg);
  box-shadow: var(--md-elev-1);
}
.mode-switch .mode-btn .material-symbols-outlined { font-size: 18px; }

/* Spine misread corrected by the verification pass. */
.spell-note {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 16px 0 0; padding: 10px 14px;
  border-radius: var(--r-lg);
  background: var(--md-tertiary-container);
  color: var(--md-on-tertiary-container);
  font-size: .85rem; line-height: 1.4;
}
.spell-note .material-symbols-outlined { font-size: 18px; flex: none; margin-top: 1px; }

/* Multi-book scan: progress through the reviewed queue. */
.queue-bar {
  display: flex; align-items: center; gap: 8px;
  margin: 0 0 20px; padding: 10px 14px;
  border-radius: var(--r-lg);
  background: var(--md-secondary-container);
  color: var(--md-on-secondary-container);
  font-size: .875rem; font-weight: 500;
}
.queue-bar .material-symbols-outlined { font-size: 20px; }

/* ---------- Collection view switcher (All / Shelves / Authors) ----------
   Same segmented-button language as .mode-switch, but content-sized and
   left-aligned rather than stretched across the container. */
/* Title on the left, switcher on the right, sharing one line. Wraps to its
   own line on narrow screens rather than squeezing the heading. */
.page-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
}
.page-head h1 { margin: 8px 0 4px; }

/* Group pages (/author/…, /series/…, /shelf/…) hide both grouping switchers,
   so this is the only way back — and it is in the flow of the page rather than
   in the panel, because below 900px the panel is behind the hamburger. */
.back {
  display: inline-flex; align-items: center; gap: 4px; margin-top: 8px;
  font-size: 14px; font-weight: 600; letter-spacing: .02em;
  color: var(--md-on-surface-variant); text-decoration: none;
}
.back:hover { color: var(--md-primary); text-decoration: none; }
.back .material-symbols-outlined { font-size: 18px; }

/* The reference's filter control: separate pills riding inside a rounded
   track, with the selected one filled rather than merely tinted. The dividing
   lines are gone — the fill carries the state on its own. */
.view-switch {
  display: inline-flex; gap: 2px;
  padding: 4px;
  background: var(--md-surface-container);
  border: 1px solid var(--md-outline-variant);
  border-radius: var(--r-full);
  max-width: 100%; overflow-x: auto;
  scrollbar-width: none;
}
.view-switch::-webkit-scrollbar { display: none; }
.view-switch .view-btn {
  padding: 8px 20px; min-height: 34px; flex-shrink: 0; white-space: nowrap;
  border: 0; border-radius: var(--r-full); box-shadow: none; cursor: pointer;
  background: transparent; color: var(--md-on-surface-variant);
  font-size: 14px; font-weight: 600; letter-spacing: .03em;
  transition: background .2s ease, color .2s ease, box-shadow .2s ease;
}
.view-switch .view-btn:hover { background: var(--md-surface-variant); box-shadow: none; }
.view-switch .view-btn[aria-pressed="true"] {
  background: var(--switch-active-bg);
  color: var(--switch-active-fg);
  box-shadow: var(--md-elev-1);
}

/* The grouping pills are the last thing in .page-head, so on a phone the
   search box lands directly beneath them with nothing in between and the two
   controls read as one crowded block. The gap belongs to the pills rather than
   to .filter-bar, because the pills are the only reason it is needed: on /tbr
   and the group pages #viewSwitch carries .hidden and the search sits under
   the <h1>, where the heading's own spacing is already right — which is the
   layout to leave alone. Above 900px the min-width block near the top of this
   file hides the pills outright, so this costs nothing there, and the releases
   tab's switch is #relSwitch and is untouched. */
@media (max-width: 899px) {
  #viewSwitch { margin-bottom: 14px; }
}

/* ---------- Collection filter bar ----------
   Search is always on screen; the facets live behind one button, because a row
   of five controls above the grid competes with the covers for attention. What
   is active is never hidden even so: every set facet draws a chip below the
   bar (.active-filters), so a short grid always explains itself. */
.filter-bar {
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  margin: 0 0 12px;
}
.search-box {
  position: relative; display: flex; align-items: center;
  flex: 1 1 260px; min-width: 0;
}
.search-box > .material-symbols-outlined {
  position: absolute; left: 12px; font-size: 20px; pointer-events: none;
  color: var(--md-on-surface-variant);
}
.search-box input {
  width: 100%; padding: 12px 40px; font: inherit; font-size: .92rem;
  color: var(--md-on-surface); background: var(--md-surface-container);
  border: 1px solid var(--md-outline-variant, rgba(0,0,0,.2));
  border-radius: var(--r-full); outline: none;
  transition: background .15s ease, border-color .15s ease;
}
.search-box input:hover { background: var(--md-surface-variant); }
.search-box input:focus {
  border-color: var(--md-primary); background: var(--md-surface-container);
}
/* The browser's own clear affordance lands exactly where ours does and draws a
   second cross beside it. */
.search-box input::-webkit-search-cancel-button { display: none; }
.q-clear {
  position: absolute; right: 6px; display: flex; padding: 4px;
  background: transparent; color: var(--md-on-surface-variant);
  border: 0; box-shadow: none; border-radius: 50%; cursor: pointer;
}
.q-clear:hover { background: var(--md-surface-variant); box-shadow: none; }
.q-clear .material-symbols-outlined { font-size: 18px; }

.facets { position: relative; flex: none; }
.facet-toggle {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 11px 18px; font-size: .85rem; font-weight: 600;
  background: var(--md-surface-container); color: var(--md-on-surface);
  border: 1px solid var(--md-outline-variant, rgba(0,0,0,.2));
  border-radius: var(--r-full); box-shadow: none; cursor: pointer;
}
.facet-toggle:hover { background: var(--md-surface-variant); box-shadow: none; }
.facet-toggle[aria-expanded="true"] { border-color: var(--md-primary); }
.facet-toggle .material-symbols-outlined { font-size: 18px; }
.facet-toggle .n {
  display: inline-grid; place-items: center; min-width: 18px; height: 18px;
  padding: 0 5px; border-radius: var(--r-full); font-size: .7rem;
  background: var(--md-primary); color: var(--md-on-primary);
}
/* Right-aligned: the button sits at the end of the bar, so a left-aligned
   panel would hang off the viewport on a phone. */
.facet-panel {
  position: absolute; right: 0; z-index: 20; margin-top: 6px;
  width: 280px; max-width: calc(100vw - 32px); padding: 6px;
  background: var(--md-surface-container-high);
  border: 1px solid var(--md-outline-variant, rgba(0,0,0,.2));
  border-radius: var(--r-xl); box-shadow: var(--md-elev-2);
  max-height: min(70vh, 480px); overflow-y: auto;
}
.facet { padding: 8px 10px; }
.facet-label {
  display: block; margin-bottom: 6px; font-size: .72rem; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--md-on-surface-variant);
}
.facet-label em {
  font-style: normal; font-weight: 400; text-transform: none; letter-spacing: 0;
}
.facet select {
  width: 100%; padding: 9px 12px; font: inherit; font-size: .85rem;
  color: var(--md-on-surface); background: var(--md-surface);
  border: 1px solid var(--md-outline-variant, rgba(0,0,0,.2));
  border-radius: var(--r-lg); cursor: pointer;
}
.facet select:disabled {
  color: var(--md-on-surface-variant); cursor: default;
  background: var(--md-surface-variant);
}
.facet-foot {
  display: flex; justify-content: flex-end; margin-top: 4px;
  padding: 6px 4px 2px;
  border-top: 1px solid var(--md-outline-variant, rgba(0,0,0,.2));
}
.facet-foot button {
  padding: 8px 14px; font-size: .8rem; background: transparent;
  color: var(--md-primary); border: 0; box-shadow: none;
}
.facet-foot button:hover { background: var(--md-surface-variant); box-shadow: none; }

.active-filters { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 14px; }
.active-filters .chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 4px 6px 4px 12px; border-radius: var(--r-full); font-size: .8rem;
  background: var(--md-secondary-container, var(--md-surface-container-high));
  color: var(--md-on-secondary-container, var(--md-on-surface));
}
.active-filters .chip button {
  display: flex; padding: 0 4px; border: 0; background: transparent;
  color: inherit; font-size: 1rem; line-height: 1;
  border-radius: 50%; box-shadow: none; cursor: pointer;
}
.active-filters .chip button:hover { background: rgba(0,0,0,.12); box-shadow: none; }
.active-filters .chip .material-symbols-outlined { font-size: 16px; }
.active-filters .clear-all {
  padding: 5px 14px; font-size: .78rem; background: transparent;
  color: var(--md-primary); border: 1px solid var(--md-outline);
  border-radius: var(--r-full); box-shadow: none; cursor: pointer;
}
.active-filters .clear-all:hover { background: var(--md-surface-variant); box-shadow: none; }

/* The empty state is prose with a link in it, so its undo has to read as one
   too rather than as a filled button dropped into a sentence. */
.link-btn {
  padding: 0; background: transparent; color: var(--md-primary);
  border: 0; box-shadow: none; font: inherit; font-weight: 600;
  text-decoration: underline; cursor: pointer;
}
.link-btn:hover { box-shadow: none; }

/* ---------- Single cover (manual add: official art or nothing) ---------- */
.cover-single { margin: 0 0 24px; }
.cover-single img {
  width: 140px; height: auto; max-height: 210px; object-fit: contain;
  border-radius: var(--r-xl); box-shadow: var(--md-elev-1); display: block; margin-bottom: 8px;
}
.cover-single .choice-label {
  font-size: .875rem; color: var(--md-on-surface-variant); margin: 0;
}

/* ---------- Cover choice (photo vs official) ---------- */
.cover-choice { margin: 0 0 24px; }
.cover-choice .choice-label {
  font-size: .875rem; color: var(--md-on-surface-variant); margin-bottom: 12px;
}
.cover-choice .choice-options { display: flex; gap: 16px; flex-wrap: wrap; }
.cover-choice .choice {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 12px; border-radius: var(--r-xl); cursor: pointer;
  border: 2px solid var(--md-outline-variant, rgba(0,0,0,.2));
  transition: border-color .15s ease, background .15s ease;
}
.cover-choice .choice:hover { background: var(--md-surface-container-high); }
.cover-choice .choice:has(input:checked) {
  border-color: var(--md-primary); background: var(--md-surface-container-high);
}
.cover-choice .choice img {
  width: 120px; height: auto; max-height: 180px; object-fit: contain;
  border-radius: var(--r-lg); box-shadow: var(--md-elev-1);
}
.cover-choice .choice span { font-size: .85rem; color: var(--md-on-surface); }
.cover-choice .choice input { accent-color: var(--md-primary); }

/* ---------- Tag picker ---------- */
.tag-picker { position: relative; margin: 0 0 20px; }
.tag-picker .choice-label {
  font-size: .8rem; color: var(--md-on-surface-variant); margin-bottom: 8px;
}
.tag-picker .tag-chips {
  display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; min-height: 28px;
  align-items: center;
}
.tag-picker .tag-none { font-size: .85rem; color: var(--md-on-surface-variant); }
.tag-picker .tag-chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 4px 6px 4px 12px; border-radius: var(--r-full); font-size: .8rem;
  background: var(--md-secondary-container, var(--md-surface-container-high));
  color: var(--md-on-secondary-container, var(--md-on-surface));
}
.tag-picker .tag-chip-x {
  border: 0; background: transparent; cursor: pointer; padding: 0 4px;
  color: inherit; font-size: 1rem; line-height: 1; border-radius: 50%;
  box-shadow: none;
}
.tag-picker .tag-chip-x:hover { background: rgba(0,0,0,.12); }
.tag-picker .tag-toggle { padding: 8px 16px; font-size: .8rem; gap: 8px; }
.tag-picker .tag-toggle .material-symbols-outlined { font-size: 18px; }
.tag-picker .tag-panel {
  position: absolute; z-index: 20; margin-top: 6px; width: 300px; max-width: 100%;
  background: var(--md-surface-container-high);
  border: 1px solid var(--md-outline-variant, rgba(0,0,0,.2));
  border-radius: var(--r-xl); box-shadow: var(--md-elev-2); overflow: hidden;
}
.tag-picker .tag-options { max-height: 240px; overflow-y: auto; padding: 8px; }
.tag-picker .tag-option {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px; border-radius: var(--r-lg); cursor: pointer; font-size: .88rem;
  color: var(--md-on-surface);
}
.tag-picker .tag-option:hover { background: var(--md-surface-variant); }
.tag-picker .tag-option input { accent-color: var(--md-primary); margin: 0; }
.tag-picker .tag-empty {
  padding: 12px 10px; font-size: .8rem; color: var(--md-on-surface-variant); margin: 0;
}
.tag-picker .tag-new {
  display: flex; gap: 6px; padding: 10px;
  border-top: 1px solid var(--md-outline-variant, rgba(0,0,0,.2));
}
.tag-picker .tag-new-input {
  flex: 1; min-width: 0; padding: 10px 12px; font-size: .85rem;
  border: 1px solid var(--md-outline-variant, rgba(0,0,0,.2));
  border-radius: var(--r-lg); background: var(--md-surface); color: var(--md-on-surface);
}
.tag-picker .tag-new-add { padding: 8px 14px; font-size: .8rem; }

/* ---------- Shelf picker ---------- */
/* Single-select sibling of the tag picker: same panel, but the trigger shows
   the current shelf like a <select> rather than a row of chips. */
.shelf-picker { position: relative; margin: 20px 0; }
/* Beside a .field in a .row, take that field's spacing so the two line up. */
.row > .shelf-picker { margin: 14px 0 0; }
/* Sharing a row with the shelf picker: same top spacing, and neither may
   collapse below a usable width before the row wraps. */
.row > .tag-picker { margin: 14px 0 0; }
.row > .shelf-picker, .row > .tag-picker { min-width: 220px; }
.shelf-picker .choice-label {
  font-size: .8rem; color: var(--md-on-surface-variant); margin-bottom: 8px;
}
.shelf-picker .shelf-toggle {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; text-align: left; padding: 14px 16px; font-size: .9rem;
  background: var(--md-surface-container-high); color: var(--md-on-surface);
  border: 1px solid var(--md-outline-variant, rgba(0,0,0,.2));
  border-radius: var(--r-xl); box-shadow: none; cursor: pointer;
}
.shelf-picker .shelf-toggle:hover { background: var(--md-surface-variant); }
.shelf-picker .shelf-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.shelf-picker .shelf-value--empty { color: var(--md-on-surface-variant); }
.shelf-picker .shelf-toggle .material-symbols-outlined { font-size: 20px; flex: none; }
.shelf-picker .shelf-panel {
  position: absolute; z-index: 20; margin-top: 6px; width: 300px; max-width: 100%;
  background: var(--md-surface-container-high);
  border: 1px solid var(--md-outline-variant, rgba(0,0,0,.2));
  border-radius: var(--r-xl); box-shadow: var(--md-elev-2); overflow: hidden;
}
.shelf-picker .shelf-options { max-height: 240px; overflow-y: auto; padding: 8px; }
.shelf-picker .shelf-option {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  padding: 10px 12px; border-radius: var(--r-lg); cursor: pointer; font-size: .88rem;
  background: transparent; border: 0; box-shadow: none; color: var(--md-on-surface);
}
.shelf-picker .shelf-option:hover { background: var(--md-surface-variant); }
.shelf-picker .shelf-option.is-selected { color: var(--md-primary); font-weight: 600; }
/* Reserve the tick column on every row so the labels line up whether or not
   the row is the selected one. */
.shelf-picker .shelf-tick { font-size: 18px; width: 18px; flex: none; }
.shelf-picker .shelf-new {
  display: flex; gap: 6px; padding: 10px;
  border-top: 1px solid var(--md-outline-variant, rgba(0,0,0,.2));
}
.shelf-picker .shelf-new-input {
  flex: 1; min-width: 0; padding: 10px 12px; font-size: .85rem;
  border: 1px solid var(--md-outline-variant, rgba(0,0,0,.2));
  border-radius: var(--r-lg); background: var(--md-surface); color: var(--md-on-surface);
}
.shelf-picker .shelf-new-add { padding: 8px 14px; font-size: .8rem; }

/* ---------- Already-owned checkboxes ---------- */
.owned { border: 0; padding: 0; margin: 0 0 8px; }
.owned legend {
  font-size: .8rem; color: var(--md-on-surface-variant);
  padding: 0; margin-bottom: 8px;
}
.owned .check {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 14px 8px 10px; margin: 0 8px 8px 0;
  border: 1px solid var(--md-outline-variant, rgba(0,0,0,.2));
  border-radius: var(--r-full); cursor: pointer; font-size: .85rem;
  color: var(--md-on-surface);
  transition: background .15s ease, border-color .15s ease;
}
.owned .check:hover { background: var(--md-surface-container-high); }
.owned .check:has(input:checked) {
  border-color: var(--md-primary);
  background: var(--md-secondary-container, var(--md-surface-container-high));
  color: var(--md-on-secondary-container, var(--md-on-surface));
}
.owned .check input { accent-color: var(--md-primary); margin: 0; }
.owned .check .material-symbols-outlined { font-size: 18px; }

/* ---------- TBR position picker (add-book review card) ---------- */
.slot .choices { display: flex; flex-wrap: wrap; }
.slot .check .material-symbols-outlined { font-size: 18px; }
.slot .hint {
  font-size: .78rem; color: var(--md-on-surface-variant); margin: 0 0 4px;
}

/* ---------- TBR position controls (detail modal) ---------- */
/* ---------- Generated cover ---------- */
/* Shown when a book has no cover, or when the image fails to load. Prints the
   title and author on a tinted card shaped like a book, so a coverless book
   still reads as a book rather than as a hole in the grid. The hue comes from
   the title, so it is stable per book and varied across a shelf. */
.fake-cover {
  aspect-ratio: 2 / 3;
  display: flex; flex-direction: column; justify-content: center;
  gap: 8px; padding: 14px 14px 14px 20px;
  background: hsl(var(--fc-h, 280) var(--fc-s) var(--fc-l));
  color: var(--fc-ink);
  overflow: hidden; position: relative;
  text-align: left;
}
/* A spine, so it reads as a book at a glance rather than a coloured tile. */
.fake-cover::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 7px;
  border-left: 1px solid var(--fc-rule);
}
.fake-cover .fc-title {
  font-weight: 600; font-size: .92rem; line-height: 1.25;
  display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical;
  overflow: hidden;
}
.fake-cover .fc-author {
  font-size: .74rem; opacity: .8; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* In the grid the card supplies the aspect ratio and full width. `display` has
   to be restated: `.book .cover` sets `block`, and it outranks `.fake-cover`,
   so without this the flex centering above never applies and the text piles up
   against the top edge. */
/* `background` has to be restated here for the same reason `display` does:
   `.book .cover` sets both for the <img> case and outranks `.fake-cover`, so
   without this every generated cover in the grid renders as one flat surface
   colour and the per-title hue is lost. */
.book .cover.fake-cover {
  width: 100%; display: flex;
  background: hsl(var(--fc-h, 280) var(--fc-s) var(--fc-l));
}
/* A rank or series-number badge floats over the top-left corner, which is
   exactly where a long generated title reaches. Keep a lane clear for it. */
.book:has(.rank) .cover.fake-cover { padding-top: 40px; }
/* In the modal it stands in for the <img>, which is 200px wide. */
.modal .fake-cover { width: 200px; max-width: 100%; border-radius: var(--r-xl); }

/* ---------- TBR group ---------- */
/* The pile-only fields, boxed together directly under the reading status so
   they read as one thing: what matters while the book is still to-read.
   Lighter than the modal panel so the group stands out rather than blending
   into the surrounding fields, via a per-theme token — no single overlay
   alpha lifts a pale surface and a dark one by the same visible amount. */
.tbr-group {
  margin: 14px 0 0; padding: 14px; border-radius: var(--r-xl);
  background: var(--tbr-panel);
  border: 1px solid var(--md-outline-variant, rgba(0,0,0,.12));
}
/* The panel's own padding already provides the gap the field would add. */
.tbr-group > .field:first-child { margin-top: 0; }
/* The panel is already a surface, so a filled input on top of it stacks two
   backgrounds for one box. The underline is what marks it editable; the fill
   was only ever doing that job on a bare page. Hover and focus are included,
   or the fill would flash back on interaction. */
.tbr-group .field textarea,
.tbr-group .field textarea:hover,
.tbr-group .field textarea:focus { background: transparent; }
/* Detail modal puts the position controls here; the add page puts the
   "add to your TBR at..." fieldset. Both want the same gap under the reason
   box, and neither wants a trailing margin fighting the panel's padding. */
.tbr-group .position { margin: 16px 0 0; }
.tbr-group > .owned { margin: 16px 0 0; }

.position { margin: 16px 0 4px; }
.position .choice-label {
  font-size: .8rem; color: var(--md-on-surface-variant); margin-bottom: 8px;
}
.position .position-btns { display: flex; gap: 8px; flex-wrap: wrap; }
.position .position-btns button { padding: 8px 14px; font-size: .8rem; gap: 6px; }
.position .position-btns .material-symbols-outlined { font-size: 18px; }
.position .position-now {
  font-size: .78rem; color: var(--md-on-surface-variant); margin: 10px 0 0;
}

.row { display: flex; gap: 16px; flex-wrap: wrap; }
.row > * { flex: 1; min-width: 160px; }
/* Rides alongside a full-width field instead of claiming a row: a series
   number is two characters, and the review form is long enough already. */
.row > .field--num { flex: 0 1 96px; min-width: 84px; }
.actions { display: flex; gap: 8px; margin-top: 24px; flex-wrap: wrap; }

.hidden { display: none !important; }

/* ---------- Chips ---------- */
.confidence {
  display: inline-flex; align-items: center;
  font-size: .75rem; font-weight: 500;
  color: var(--md-on-secondary-container);
  background: var(--md-secondary-container);
  padding: 4px 12px; border-radius: var(--r-lg); margin-left: 8px;
}

.badge {
  display: inline-flex; align-items: center;
  font-size: 10px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
  padding: 3px 8px; border-radius: var(--r-full); margin-top: 8px;
}
.badge.to-read { background: var(--md-surface-variant); color: var(--md-on-surface-variant); }
.badge.reading { background: var(--md-primary-container); color: var(--md-on-primary-container); }
.badge.read { background: var(--md-tertiary-container); color: var(--md-on-tertiary-container); }

/* ---------- Spinner ---------- */
.spinner {
  display: inline-block; width: 18px; height: 18px; vertical-align: middle;
  border: 2px solid currentColor; border-right-color: transparent;
  opacity: .9; border-radius: 50%; animation: spin .7s linear infinite;
  flex-shrink: 0;
}
/* Buttons lay their contents out with flex and a gap, so a spinner dropped in
   place of an icon needs to keep its size rather than being squeezed. */
button > .spinner { margin-right: 0; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- Ripple ---------- */
.ripple {
  position: absolute; border-radius: 50%; transform: scale(0);
  background: currentColor; opacity: .28; pointer-events: none;
  animation: ripple .6s ease-out;
}
@keyframes ripple { to { transform: scale(2.5); opacity: 0; } }

/* ---------- Login overlay (MD3 dialog) ---------- */
.overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,.32);
  display: flex; align-items: center; justify-content: center; z-index: 50; padding: 24px;
}
.overlay .card {
  background: var(--md-surface-container-high);
  max-width: 360px; width: 100%; text-align: center;
  border-radius: var(--r-xl); padding: 24px; box-shadow: var(--md-elev-3);
  margin: 0;
}

/* ---------- Collection grid ---------- */
.grid {
  display: grid; gap: 16px;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}

/* Grouped views (Shelves / Authors): the outer container stops being a grid
   and becomes a stack of sections, each with its own grid inside. */
.grid.grouped { display: block; }
.grid.grouped .group + .group { margin-top: 32px; }
.group-head {
  /* Wraps rather than overflows. A series heading can carry seven things —
     star, name, count, average, publication status, progress select and the
     View more link — which is more than a phone fits on one line, and a
     heading that scrolls sideways would hide the link that is the way into
     the group. Wrapping the row never fragments `.glabel` itself, so the
     view-transition-name on it still survives. */
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap; row-gap: 6px;
  font-size: 1rem; font-weight: 500; letter-spacing: .1px;
  color: var(--md-on-surface-variant);
  margin: 0 0 12px; padding-bottom: 8px;
  border-bottom: 1px solid var(--md-outline-variant);
}
/* Inline-block so the label is a single box: a view-transition-name on an
   element that fragments across two lines is dropped by the browser, and the
   morph into the group page's title silently becomes a crossfade. */
.group-head .glabel { display: inline-block; }
.group-head .n {
  font-size: .75rem; font-weight: 500;
  background: var(--md-secondary-container); color: var(--md-on-secondary-container);
  border-radius: var(--r-full); padding: 2px 8px;
}

/* The author's average, next to the book count it is drawn from. */
.group-head .avg {
  font-size: .75rem; font-weight: 600;
  background: var(--md-surface-container-high); color: var(--md-on-surface);
  border-radius: var(--r-full); padding: 2px 8px; white-space: nowrap;
}

/* The door to the group's own page, at the far end of the heading. Offered by
   every group, not only the ones that overflow: it is how you get to the whole
   of a group, and a control that comes and goes with the window width would
   be harder to find than one that is always there. */
.group-head .more {
  margin-left: auto; flex-shrink: 0;
  display: inline-flex; align-items: center; gap: 2px; white-space: nowrap;
  font-size: .78rem; font-weight: 600; letter-spacing: .02em;
  color: var(--md-primary); text-decoration: none;
  border-radius: var(--r-full); padding: 4px 6px 4px 10px;
  transition: background .15s ease;
}
.group-head .more:hover {
  background: var(--md-surface-container-high); text-decoration: none;
}
.group-head .more .material-symbols-outlined { font-size: 18px; }

/* ---------- Release panel (author pages) ----------
   Forthcoming and recently-out books by this author, above their shelf. Its
   own surface colour rather than another card: the point is that these are
   NOT in the collection, and a panel that matched the grid would read as more
   of the same books. The cards inside are the release tab's, unchanged. */
.rel-panel {
  background: var(--rel-panel-bg);
  border: 1px solid var(--rel-panel-border);
  border-radius: var(--r-xl);
  padding: 16px 16px 4px;
  margin: 0 0 28px;
}
.rel-panel-head {
  display: flex; align-items: center; gap: 8px;
  margin: 0 0 4px; font-size: 1rem; font-weight: 600; letter-spacing: .1px;
  color: var(--md-on-surface);
}
.rel-panel-head .material-symbols-outlined { font-size: 20px; color: var(--md-primary); }
.rel-panel-head .n {
  font-size: .75rem; font-weight: 500;
  background: var(--md-primary); color: var(--md-on-primary);
  border-radius: var(--r-full); padding: 2px 8px;
}
.rel-panel-head .more {
  margin-left: auto; flex-shrink: 0;
  display: inline-flex; align-items: center; gap: 2px; white-space: nowrap;
  font-size: .78rem; font-weight: 600; letter-spacing: .02em;
  color: var(--md-primary); text-decoration: none;
}
.rel-panel-head .more:hover { text-decoration: underline; }
.rel-panel-head .more .material-symbols-outlined { font-size: 18px; }
.rel-panel .sub {
  margin: 0 0 4px; font-size: 13px; color: var(--md-on-surface-variant);
}
/* The cards sit in the same scrolling row the collection groups use, so a
   prolific author's forthcoming list cannot push the shelf off the screen. */
.rel-panel .grid.scroll-row { padding-top: 8px; }
.rel-panel .book.release { cursor: default; }
.rel-panel .book.release .t a { color: inherit; text-decoration: none; }
.rel-panel .book.release .t a:hover { color: var(--md-primary); text-decoration: underline; }

/* One row per group, scrolled sideways rather than wrapped. A group in the
   collection view is a glance at what is in it — "View more" opens the whole
   thing — so the row is a fixed height however many books it holds, and the
   page stays a list of groups rather than a wall.
   The vertical padding is not decoration: `overflow-x: auto` makes the y axis
   scroll too, so without room the card's hover lift and the cover's shadow
   would both be clipped. Cards are a fixed width here rather than the grid's
   `1fr`, since a flexible track in a scroller either stretches four books
   across the whole row or hides the fact that there are twenty. */
.group .grid.scroll-row {
  /* `nowrap` is declared, not assumed. This class was called `row` once, which
     collided with the form-layout `.row` utility above (`flex-wrap: wrap`,
     `> * { flex: 1 }`): the container went flex as intended and then wrapped
     anyway, which looks exactly like the rule never applied. */
  display: flex; flex-wrap: nowrap; gap: 16px; padding: 6px 0 10px;
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
  scrollbar-color: var(--md-outline-variant) transparent;
}
.group .grid.scroll-row > .book { flex: 0 0 160px; scroll-snap-align: start; }
.group .grid.scroll-row::-webkit-scrollbar { height: 8px; }
.group .grid.scroll-row::-webkit-scrollbar-track { background: transparent; }
.group .grid.scroll-row::-webkit-scrollbar-thumb {
  background: var(--md-outline-variant); border-radius: var(--r-full);
}
/* ---------- Future releases ----------
   The cards reuse .book wholesale so the tab reads as the collection grid; the
   only differences are that each one is a link out to Open Library and that
   the corner chip carries a countdown rather than a rank. */
.book.release { display: block; text-decoration: none; color: inherit; }
.book.release:hover { text-decoration: none; }
.book.release .rank.seq {
  left: 8px; right: auto; padding: 0 10px;
  font-size: .7rem; font-weight: 600; letter-spacing: .02em;
  text-transform: uppercase; white-space: nowrap;
}
/* A release found for the first time by the latest scan. The tint is a step
   off the page rather than a highlight — this is "you have not seen this yet",
   not "this is urgent". */
.book.release.is-new {
  background: var(--md-surface-container);
  border-radius: var(--r-lg);
  padding: 8px 8px 12px;
}
.book.release.is-new .cover { border-radius: calc(var(--r-lg) - 4px); }
.book.release.is-new .meta { padding-left: 4px; padding-right: 4px; }
.new-flag {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  padding: 3px 9px; border-radius: var(--r-full);
  font-size: 10px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
  background: var(--md-primary); color: var(--md-on-primary);
  box-shadow: var(--md-elev-1);
}

/* Triage: keep leaves the card and retires these, delete removes it. */
.release .triage { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
.release .triage button {
  flex: 1; min-width: 0; padding: 6px 10px; min-height: 32px;
  font-size: 12px; gap: 4px;
}
.release .triage button .material-symbols-outlined { font-size: 16px; }
.release .triage button[data-mark="dismissed"]:hover {
  border-color: var(--md-error); color: var(--md-error);
}
/* Recent releases only: a route into the normal add-a-book flow. Filled,
   because it is the affirmative action on the card while Keep and Delete are
   both housekeeping. */
.release .add-link {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  margin-top: 10px; padding: 7px 14px; width: 100%;
  border-radius: var(--r-full);
  background: var(--md-primary); color: var(--md-on-primary);
  font-size: 12px; font-weight: 600; letter-spacing: .03em; text-decoration: none;
}
.release .add-link:hover { text-decoration: none; box-shadow: var(--md-elev-1); }
.release .add-link .material-symbols-outlined { font-size: 16px; }

/* Said out loud on the card, not just in a tooltip: a phone has no hover, and
   this is the difference between a date read and a date guessed. */
.book.release .assumed {
  margin-top: 6px; font-size: .7rem; letter-spacing: .02em;
  color: var(--md-on-surface-variant); font-style: italic;
}

/* The edit affordance sits with the title it edits, not in the action row —
   the action row is about the book, this is about the panel's mode. */
.detail .title-row {
  display: flex; align-items: flex-start; gap: 8px; justify-content: space-between;
}
.detail .title-row h2 { margin: 0 0 16px; flex: 1; }
/* margin-left:auto keeps the pencil on the right in edit mode, where the
   heading beside it is hidden and there is nothing left to push it over. */
.detail .title-row .icon-btn { flex-shrink: 0; margin-top: -4px; margin-left: auto; }
.detail .title-row .icon-btn[aria-pressed="true"] {
  background: var(--md-secondary-container);
  color: var(--md-on-secondary-container);
}
.detail .edit-only .row { margin-bottom: 4px; }

/* The "At position" choice carries its own number box. It sits inside the
   label so clicking anywhere on the row still selects the radio. */
.check--exact { gap: 8px; }
.check--exact .slot-input {
  width: 74px; flex-shrink: 0; margin-left: 4px;
  padding: 6px 10px; font-size: .85rem; font-family: inherit;
  color: var(--md-on-surface); background: var(--md-surface-container-high);
  border: 1px solid var(--md-outline-variant); border-radius: var(--r-lg);
}
.check--exact .slot-input:focus {
  outline: none; border-color: var(--md-primary);
}
#tbrCount { color: var(--md-on-surface-variant); }

/* Sits under whichever cover preview is showing; hidden until there is
   official art, and gone once the switch budget is spent. */
.alt-cover { margin: 0 0 20px; }

/* ---------- Cover carousel (alternative cover art) ---------- */
/* One component, two hosts: the add page's review card and the collection
   modal. Six covers is a comparison rather than a slideshow, so every
   candidate is on screen at once and the row scrolls — paging through them one
   at a time would mean holding the last one in your head to judge the next.
   Nothing here hard-codes a radius or a shadow: both read theme tokens, so the
   strip takes on each theme's shape like every other component. */
.cover-carousel { position: relative; margin: 0 0 20px; }
.cover-carousel .car-track {
  display: flex; gap: 8px; overflow-x: auto; scroll-behavior: smooth;
  /* overflow-x scrolls the y axis too, so the hover lift and the cover shadow
     need room or they are clipped — the same trap as .grid.scroll-row. */
  padding: 6px 2px 8px;
  scrollbar-width: thin;
}
.cover-carousel .car-item {
  flex: 0 0 auto; padding: 0; line-height: 0; cursor: pointer;
  background: none; border: 2px solid transparent;
  border-radius: var(--r-lg);
  transition: border-color .15s ease, transform .15s ease;
}
.cover-carousel .car-item:hover { transform: translateY(-2px); }
.cover-carousel .car-item.on { border-color: var(--md-primary); }
.cover-carousel .car-item img {
  width: 56px; height: 84px; object-fit: cover; display: block;
  border-radius: calc(var(--r-lg) - 2px);
  box-shadow: var(--cover-shadow, var(--md-elev-1));
}
/* The arrows overlay the ends of the strip. They are only for pointers that
   cannot flick: the track scrolls by touch and by wheel regardless. */
.cover-carousel .car-arrow {
  position: absolute; top: 40px; z-index: 2;
  width: 26px; height: 26px; padding: 0; border: none; cursor: pointer;
  border-radius: var(--r-full); display: grid; place-items: center;
  background: var(--md-surface-container-high); color: var(--md-on-surface);
  box-shadow: var(--md-elev-1);
}
.cover-carousel .car-arrow.prev { left: -8px; }
.cover-carousel .car-arrow.next { right: -8px; }
.cover-carousel .car-arrow .material-symbols-outlined { font-size: 18px; }
.cover-carousel .car-note {
  margin: 2px 0 0; font-size: .75rem; color: var(--md-on-surface-variant);
}
@media (prefers-reduced-motion: reduce) {
  .cover-carousel .car-track { scroll-behavior: auto; }
  .cover-carousel .car-item:hover { transform: none; }
}

/* ---------- Release schedule sections ---------- */
.rel-section + .rel-section { margin-top: 40px; }
.rel-section > h2 { margin: 0 0 4px; }
.section-note {
  margin: 0 0 20px; font-size: 14px; color: var(--md-on-surface-variant);
}

/* ---------- Release calendar ---------- */
.head-tools { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

.cal { max-width: 520px; }
.cal-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 12px;
}
.cal-head h2 { margin: 0; font-size: 20px; }
.cal-nav {
  width: 36px; height: 36px; min-height: 36px; padding: 0; flex-shrink: 0;
  border-radius: var(--r-full); border: 1px solid var(--md-outline-variant);
  background: transparent; color: var(--md-on-surface-variant);
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: none; cursor: pointer;
}
.cal-nav:hover { background: var(--md-surface-container-high); box-shadow: none; }
.cal-nav .material-symbols-outlined { font-size: 20px; }

.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.cal-dow {
  text-align: center; font-size: 11px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--md-on-surface-variant); padding-bottom: 4px;
}
.cal-day {
  aspect-ratio: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 3px;
  border-radius: var(--r-lg); font-size: 14px;
  color: var(--md-on-surface); background: var(--md-surface-container-low);
}
.cal-day.pad { background: transparent; color: var(--md-on-surface-variant); opacity: .35; }
/* Today is context, not content: an outline, so it never competes with a dot. */
.cal-day.today { outline: 2px solid var(--md-outline); outline-offset: -2px; }
.cal-day.has { background: var(--md-secondary-container); color: var(--md-on-secondary-container); font-weight: 600; }
/* Already out: present on the timeline, but visibly behind you. */
.cal-day.has.past { background: var(--md-surface-variant); color: var(--md-on-surface-variant); }
.cal-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--md-primary);
}
/* Hollow repeats the card's distinction: the day was assumed, not read. */
.cal-dot.assumed { background: transparent; box-shadow: inset 0 0 0 1.5px var(--md-primary); }
.cal-dot.past { background: var(--md-outline); }
.cal-dot.past.assumed { background: transparent; box-shadow: inset 0 0 0 1.5px var(--md-outline); }
.cal-nav[disabled] { opacity: .35; cursor: default; }
.cal-nav[disabled]:hover { background: transparent; }
.cal-item.past .d { color: var(--md-on-surface-variant); }

.cal-list { margin-top: 20px; display: flex; flex-direction: column; gap: 4px; }
.cal-item {
  display: flex; gap: 12px; align-items: baseline;
  padding: 10px 12px; border-radius: var(--r-lg);
  background: var(--md-surface-container-low);
  color: inherit; text-decoration: none;
}
.cal-item:hover { background: var(--md-surface-container); text-decoration: none; }
.cal-item .d {
  flex-shrink: 0; min-width: 34px; text-align: center;
  font-size: 18px; font-weight: 700; color: var(--md-primary);
}
.cal-item .t { font-size: 14px; font-weight: 600; }
.cal-item .a { font-size: 13px; color: var(--md-on-surface-variant); margin-top: 2px; }
.cal-item .assumed { font-size: 11px; font-style: italic; color: var(--md-on-surface-variant); margin-top: 2px; }
.cal-none { margin-top: 20px; color: var(--md-on-surface-variant); font-size: 14px; }

/* The scan is slow enough to need a real header control rather than a link. */
.page-head #scanBtn { flex-shrink: 0; }
.page-head #scanBtn .spinner { margin-right: 2px; }

/* Favourite-author toggle. Sits before the name so the starred rows line up
   down the left edge and can be scanned without reading. The FILL axis is
   opened to 0..1 in the font URL, which is what lets one glyph be both the
   hollow and the solid star. */
.group-head .fav {
  width: 28px; height: 28px; min-height: 28px; padding: 0; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--r-full); box-shadow: none; cursor: pointer;
  background: transparent; color: var(--md-outline);
  transition: color .15s ease, background .15s ease;
}
.group-head .fav:hover { background: var(--md-surface-container-high); box-shadow: none; }
.group-head .fav .material-symbols-outlined { font-size: 20px; }
.group-head .fav[aria-pressed="true"] { color: var(--md-primary); }
.group-head .fav[aria-pressed="true"] .material-symbols-outlined {
  font-variation-settings: "FILL" 1;
}

/* Progress through a series, in the reader's own words rather than the
   publisher's — `.status` above says whether the books are all out, this says
   whether you have read them. Two facts, two chips, never merged: a completed
   series you have not started is a real and common thing.

   A select rather than a toggle because there are three states, and a button
   that cycles through three is a guessing game. */
.group-head .progress, .head-marks .progress {
  font-size: .72rem; font-weight: 500; letter-spacing: .2px;
  color: var(--md-on-surface-variant); background: transparent;
  border: 1px solid var(--md-outline-variant);
  border-radius: var(--r-full); padding: 2px 6px 2px 8px;
  cursor: pointer; flex-shrink: 0; max-width: 170px;
}
.group-head .progress:hover, .head-marks .progress:hover {
  background: var(--md-surface-container-high);
}
/* Marked states read as statements, not as an untouched control, and the four
   are ranked by how much they want your attention: `read` is done and gets a
   filled chip, `reading` is live and gets the accent outline, `on-hold` and
   `not-started` are quiet facts and stay outlined in the neutral. */
.group-head .progress[data-state="read"], .head-marks .progress[data-state="read"] {
  color: var(--md-on-secondary-container); background: var(--md-secondary-container);
  border-color: transparent;
}
.group-head .progress[data-state="reading"], .head-marks .progress[data-state="reading"] {
  color: var(--md-primary); border-color: var(--md-primary);
}
.group-head .progress[data-state="on-hold"], .head-marks .progress[data-state="on-hold"],
.group-head .progress[data-state="not-started"], .head-marks .progress[data-state="not-started"] {
  color: var(--md-on-surface); border-color: var(--md-outline);
}

/* The group page's copy of the grid heading's controls. */
.head-marks { display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0; }
.head-marks .fav {
  width: 32px; height: 32px; min-height: 32px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--r-full); box-shadow: none; cursor: pointer;
  background: transparent; color: var(--md-outline);
  transition: color .15s ease, background .15s ease;
}
.head-marks .fav:hover { background: var(--md-surface-container-high); box-shadow: none; }
.head-marks .fav .material-symbols-outlined { font-size: 22px; }
.head-marks .fav[aria-pressed="true"] { color: var(--md-primary); }
.head-marks .fav[aria-pressed="true"] .material-symbols-outlined {
  font-variation-settings: "FILL" 1;
}

/* Whether the series has finished. Quieter than the count so the series name
   still leads the heading. */
.group-head .status {
  font-size: .72rem; font-weight: 400; letter-spacing: .2px;
  color: var(--md-on-surface-variant);
  border: 1px solid var(--md-outline-variant);
  border-radius: var(--r-full); padding: 1px 8px;
}
/* Two looks from one set of elements, chosen by the theme's card tokens: in
   light and dark the card is transparent and the *cover* carries the shadow,
   so the covers appear to float on the page; in sparkle the card itself
   becomes a pane of translucent glass and the cover goes flat inside it. */
.book {
  position: relative; cursor: pointer;
  background: var(--card-bg);
  border: 1px solid var(--card-border);
  border-radius: var(--r-lg);
  padding: var(--card-pad);
  backdrop-filter: var(--card-blur);
  box-shadow: var(--card-shadow);
  transition: transform .3s ease, box-shadow .3s ease;
}
.book:hover {
  transform: var(--card-hover-lift);
  box-shadow: var(--card-shadow-hover);
}
.book:hover .cover { box-shadow: var(--cover-shadow-hover); }
.book .cover {
  width: 100%; aspect-ratio: 2 / 3; object-fit: cover; display: block;
  background: var(--md-surface-container);
  border-radius: calc(var(--r-lg) - var(--card-pad));
  box-shadow: var(--cover-shadow);
  transition: box-shadow .3s ease;
}
.book .meta { padding: 12px 2px 0; }
.book .meta .t {
  font-weight: 600; font-size: 14px; line-height: 1.35; letter-spacing: .01em;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.book:hover .meta .t { color: var(--md-primary); }
.book .meta .a { color: var(--md-on-surface-variant); font-size: 14px; margin-top: 2px; }
/* Reason-for-TBR hint: two lines at most, so cards stay a uniform height. */
.book .meta .why {
  margin-top: 6px; font-size: .78rem; line-height: 1.35; font-style: italic;
  color: var(--md-on-surface-variant);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Rank chip + boost button sit over the cover; .book is already positioned. */
.book .rank {
  position: absolute; top: 8px; left: 8px; z-index: 2;
  min-width: 24px; height: 24px; padding: 0 7px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-full); font-size: .78rem; font-weight: 500;
  background: var(--md-primary); color: var(--md-on-primary);
  box-shadow: var(--md-elev-1);
}
/* Same slot as the TBR rank, toned down: the number is context, not a ranking. */
.book .rank.seq {
  background: var(--md-secondary-container); color: var(--md-on-secondary-container);
}
/* The score sits in the opposite corner from the TBR rank and the
   series-number badge, so a ranked or numbered card can still show it. Muted
   rather than accent-coloured: it is a fact about the book, not a control. */
.book .score {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  min-width: 24px; height: 24px; padding: 0 7px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-full); font-size: .78rem; font-weight: 600;
  background: var(--md-surface-container-high); color: var(--md-on-surface);
  box-shadow: var(--md-elev-1);
}
/* In the TBR view the boost button already owns that corner. */
.book:has(.boost) .score { right: 44px; }

.book .boost {
  position: absolute; top: 6px; right: 6px; z-index: 2;
  width: 32px; height: 32px; padding: 0; border-radius: var(--r-full);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--md-surface-container-high); color: var(--md-on-surface);
  border: 0; cursor: pointer; opacity: .92; box-shadow: var(--md-elev-1);
}
.book .boost:hover { background: var(--md-primary); color: var(--md-on-primary); opacity: 1; }
.book .boost:disabled { opacity: .4; cursor: default; }
.book .boost .material-symbols-outlined { font-size: 20px; }

/* Rating row: two narrow boxes and the number they add up to. The boxes stay
   narrow so the row does not read as two unrelated fields. */
.rating-row { align-items: center; }
/* `.row > *` sets a 160px floor, which is wider than either of these boxes
   needs and pushed the readout onto a line of its own. Both the basis and the
   floor have to be overridden, or the flex-basis is silently ignored. */
.rating-row > .field--select { flex: 0 1 128px; min-width: 108px; }
.rating-row > .rating-out { flex: 0 0 auto; min-width: 0; }
.rating-row .rating-out {
  font-size: .8rem; color: var(--md-on-surface-variant); white-space: nowrap;
}

.empty { text-align: center; color: var(--md-on-surface-variant); padding: 64px 20px; }

/* ---------- Page transitions ----------
   Same-origin navigations crossfade instead of flashing white. It costs one
   line because the browser does the work: `View more` and the group pages are
   real links, and the two documents share most of their content, so the hard
   cut was the only thing making them feel like different pages. Browsers
   without the API navigate exactly as before. */
@view-transition { navigation: auto; }

/* The theme switch is a same-document transition and wants the opposite of a
   crossfade — the new palette wipes in from the button as a circle (the radius
   is animated from theme.js, which is the only place that knows where the
   click was). Scoped to the switching class so it cannot touch the navigation
   crossfade above, which shares these pseudo-elements. */
:root.theme-switching::view-transition-old(root),
:root.theme-switching::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}
:root.theme-switching::view-transition-new(root) { z-index: 1; }
:root.theme-switching::view-transition-old(root) { z-index: 0; }

/* ---------- Loading skeletons ----------
   The collection used to resolve into an empty grid and then fill, so the
   first thing every session showed was a blank page. These stand in at the
   real card's proportions, which also stops the layout jumping when the books
   arrive. */
.book.skeleton { pointer-events: none; }
.book.skeleton:hover { transform: none; box-shadow: var(--card-shadow); }
.sk-cover, .sk-line {
  position: relative; overflow: hidden;
  background: var(--md-surface-container-high);
}
.sk-cover {
  width: 100%; aspect-ratio: 2 / 3; display: block;
  border-radius: calc(var(--r-lg) - var(--card-pad));
}
.sk-line { height: 12px; border-radius: var(--r-sm); margin-top: 12px; }
.sk-line.short { width: 55%; margin-top: 8px; }
.sk-cover::after, .sk-line::after {
  content: ""; position: absolute; inset: 0; transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, var(--sk-sweep), transparent);
  animation: sk-shimmer 1.4s ease-in-out infinite;
}
@keyframes sk-shimmer { 100% { transform: translateX(100%); } }

/* ---------- Detail modal (MD3 dialog) ---------- */
.modal {
  position: fixed; inset: 0; background: rgba(0,0,0,.4);
  display: flex; align-items: center; justify-content: center; z-index: 40; padding: 24px;
}
.modal .panel {
  background: var(--md-surface-container-high);
  border-radius: var(--r-xl); max-width: 680px; width: 100%;
  max-height: 90vh; overflow-y: auto; box-shadow: var(--md-elev-3);
}
.modal .panel-inner { display: flex; gap: 24px; padding: 24px; flex-wrap: wrap; }
/* The cover column is the flex item now, so it carries the anti-stretch rule;
   the image keeps its natural aspect ratio inside it. */
.modal .cover-col { align-self: flex-start; width: 200px; max-width: 100%; }
.modal .panel-inner img {
  width: 200px; max-width: 100%; height: auto;
  object-fit: contain; border-radius: var(--r-xl); box-shadow: var(--md-elev-1);
}
.modal .cover-empty {
  width: 200px; max-width: 100%; aspect-ratio: 2 / 3;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px; border-radius: var(--r-xl);
  background: var(--md-surface-variant); color: var(--md-on-surface-variant);
  font-size: .8rem;
}
.modal .cover-empty .material-symbols-outlined { font-size: 28px; }
.modal .cover-actions { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; }
.modal .cover-actions button {
  width: 100%; justify-content: flex-start; gap: 8px;
  padding: 8px 14px; font-size: .8rem;
}
.modal .cover-actions button .material-symbols-outlined { font-size: 18px; }

/* ---------- Confirm dialog (MD3 alert dialog) ---------- */
.confirm {
  position: fixed; inset: 0; background: rgba(0,0,0,.4);
  display: flex; align-items: center; justify-content: center; z-index: 70; padding: 24px;
}
.confirm .confirm-panel {
  background: var(--md-surface-container-high);
  border-radius: var(--r-xl); padding: 24px; max-width: 400px; width: 100%;
  box-shadow: var(--md-elev-3);
}
.confirm h3 { font-size: 1.1rem; font-weight: 500; margin-bottom: 12px; }
.confirm p {
  font-size: .9rem; line-height: 1.5; color: var(--md-on-surface-variant); margin: 0;
}
.confirm .actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 24px; }
.modal .detail { flex: 1; min-width: 240px; }
.modal .detail h2 { margin-bottom: 12px; }
.modal .detail p { margin: 4px 0; font-size: .92rem; color: var(--md-on-surface); }
.modal .detail .k { color: var(--md-on-surface-variant); }

/* ---------- Toast (MD3 snackbar) ---------- */
#toast {
  position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(8px);
  background: var(--md-inverse-surface); color: var(--md-inverse-on-surface);
  padding: 14px 20px; border-radius: var(--r-lg);
  box-shadow: var(--md-elev-3); opacity: 0;
  transition: opacity .2s ease, transform .2s ease;
  z-index: 60; pointer-events: none; font-size: .9rem; max-width: 90vw;
}
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
#toast.error { background: var(--md-error-container); color: var(--md-on-error-container); }

/* ---------- Cursor sparkles (sparkle theme only) ----------
   theme.js drops one <span class="sparkle"> per spawn and removes it when the
   animation ends. Fixed-position + pointer-events:none so they never intercept
   clicks or affect layout. */
.sparkle {
  position: fixed;
  top: 0; left: 0;
  pointer-events: none;
  z-index: 9999;
  will-change: transform, opacity;
  background: currentColor;
  clip-path: polygon(50% 0%, 61% 39%, 100% 50%, 61% 61%, 50% 100%, 39% 61%, 0% 50%, 39% 39%);
}

/* Pop and fall — the everyday sparkle, on the trail and on every tap. The
   duration is set per particle in theme.js; this is only the fallback. */
.sparkle--pop { animation: sparkle-pop 1000ms ease-out forwards; }

@keyframes sparkle-pop {
  0%   { transform: translate(-50%, -50%) scale(0)   rotate(0deg);   opacity: 0; }
  25%  { transform: translate(-50%, -50%) scale(1)   rotate(35deg);  opacity: 1; }
  100% { transform: translate(-50%, 60%)  scale(.15) rotate(110deg); opacity: 0; }
}

/* Firework — reserved for a book joining the pile. Rises through a peak, then
   arcs back down under gravity. --dx/--peak/--fall come from theme.js, one
   vector per particle, so they don't all fly the same way. */
.sparkle--firework { animation: sparkle-firework 1150ms ease-out forwards; }

@keyframes sparkle-firework {
  0%   { transform: translate(-50%, -50%) scale(0) rotate(0deg); opacity: 0; }
  14%  { transform: translate(calc(-50% + var(--dx) * .2), calc(-50% + var(--peak) * .45))
                    scale(1) rotate(25deg); opacity: 1; }
  45%  { transform: translate(calc(-50% + var(--dx) * .6), calc(-50% + var(--peak)))
                    scale(.9) rotate(70deg); opacity: 1; }
  100% { transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--fall)))
                    scale(.25) rotate(150deg); opacity: 0; }
}

/* Motion-sensitive users get the pink palette without the flying particles. */
@media (prefers-reduced-motion: reduce) {
  .sparkle { display: none; }
  .sidenav { transition: none; }
  /* Page and theme transitions still happen — they just arrive instantly
     rather than being animated. The FLIP on the TBR and the skeleton shimmer
     are both switched off at their source, in JS and here respectively. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
  .sk-cover::after, .sk-line::after { animation: none; }
}

@media (max-width: 560px) {
  .brand { font-size: 1.125rem; }
}
