:root {
    /* Neutral off-white, no tint. The lavender radial that used to sit here
       meant purple was the page as well as the accent, so nothing the accent
       marked actually stood out. Purple is now reserved for active states. */
    color-scheme: light;
    --bg: #f8f9fa;
    --bg-gradient: none;
    --card: #ffffff;
    --text: #0f172a;
    --muted: #64748b;
    --border: #e2e8f0;
    --border-soft: #f1f5f9;
    --accent: #7c5cff;
    --accent-dark: #6544e0;
    --accent-soft: #efeaff;
    /* The "Trek" half of the wordmark. Brand only — deliberately NOT --read
       (#3f9d68), which already means "finished" on a status badge. Nothing
       but the logo should use this. */
    --brand-green: #075c36;
    /* The line of copy under a page heading. Deliberately not --muted: that is
       the tone for captions and secondary data, and at this size it made the
       sentence read as fine print under the heading rather than as part of the
       same composition. */
    --sub: #475569;
    /* Status pairs. Each of these was written twice — a light literal in the
       base rule and a dark literal inside a prefers-color-scheme block —
       which is the reason a forced theme needed the whole file duplicated.
       Defined once per theme here, they follow it on their own and the
       component rules stop carrying colours at all. */
    --ok-bg: #d1fae5;      --ok-fg: #047857;
    --warn-bg: #fef3c7;    --warn-fg: #b45309;
    --info-bg: #e0f2fe;    --info-fg: #0369a1;
    --danger-fg: #b3261e;  --danger-bg: #b3261e;
    --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04);
    --shadow-md: 0 4px 14px rgba(15, 23, 42, 0.06);
    --shadow-lg: 0 12px 32px rgba(15, 23, 42, 0.10);

    --amazon: #e58a1a;
    --kindle: #2e7dbb;
    --audible: #d97a2e;
    --waterstones: #1f7a4d;
    --other: #8a8378;

    --unread: #a89f92;
    --reading: #d9a441;
    --read: #3f9d68;
    --sample: #8a7cc4;

    /* The one column width every page and both fixed bars share. Used to be
       three independent numbers (main/header-align-wrap at 1180, .tab-nav at
       1152, the trail/goals/leaderboard family's own body at 700) picked
       separately wherever each was written — invisible as a mismatch until
       the topbar started showing on a wide screen outside an installed PWA
       and sat a visibly different width than the page content under it.
       One variable now, so a future change can't quietly drift apart again. */
    --content-max: 1180px;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme='light']) { color-scheme: dark;
        --bg: #17151a;
        --ok-bg: #064e3b;      --ok-fg: #6ee7b7;
        --warn-bg: #4a3208;    --warn-fg: #fcd34d;
        --info-bg: #0c4a6e;    --info-fg: #7dd3fc;
        --danger-fg: #f2a8a2;  --danger-bg: #b3261e;
        --bg-gradient: radial-gradient(circle at 15% 0%, #241f2e 0%, #17151a 40%);
        /* Card/border were only a few RGB points off the page background —
           a real numeric gap, but compressed into such a dark range it read
           as visually flat (near-black tones need a much bigger jump than
           bright ones to look distinct to the eye, and box-shadow is
           useless for depth here since a dark shadow doesn't show against
           an already-dark page). Widened both so surfaces actually read as
           raised cards instead of same-color rectangles. */
        --card: #29242f;
        --text: #f1ede6;
        --muted: #9c9389;
        --border: #453e51;
        --border-soft: #322c3a;
        --accent: #9c85ff;
        --accent-dark: #b3a0ff;
        --accent-soft: #2c2640;
        --brand-green: #7fe0b0;
        --sub: #bab1a6;
        --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
        --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.35);
        --shadow-lg: 0 16px 36px rgba(0, 0, 0, 0.45); }
}
:root[data-theme='dark'] { color-scheme: dark;
        --bg: #17151a;
        --ok-bg: #064e3b;      --ok-fg: #6ee7b7;
        --warn-bg: #4a3208;    --warn-fg: #fcd34d;
        --info-bg: #0c4a6e;    --info-fg: #7dd3fc;
        --danger-fg: #f2a8a2;  --danger-bg: #b3261e;
        --bg-gradient: radial-gradient(circle at 15% 0%, #241f2e 0%, #17151a 40%);
        /* Card/border were only a few RGB points off the page background —
           a real numeric gap, but compressed into such a dark range it read
           as visually flat (near-black tones need a much bigger jump than
           bright ones to look distinct to the eye, and box-shadow is
           useless for depth here since a dark shadow doesn't show against
           an already-dark page). Widened both so surfaces actually read as
           raised cards instead of same-color rectangles. */
        --card: #29242f;
        --text: #f1ede6;
        --muted: #9c9389;
        --border: #453e51;
        --border-soft: #322c3a;
        --accent: #9c85ff;
        --accent-dark: #b3a0ff;
        --accent-soft: #2c2640;
        --brand-green: #7fe0b0;
        --sub: #bab1a6;
        --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
        --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.35);
        --shadow-lg: 0 16px 36px rgba(0, 0, 0, 0.45); }


* { box-sizing: border-box; }

/* Browsers give <button> its own default font (Chrome: ~13.3px) instead of
   inheriting the surrounding text size the way every other element does —
   invisible for buttons that only contain plain text, but .icon sizes
   itself in em, so any icon-only button silently rendered ~40% smaller
   than an equivalent icon inside an <a>, with no difference in the
   markup to explain it. Exactly what made the topbar's gift (an <a>) and
   settings (a <button>) icons mismatched despite identical 34px boxes. */
button { font: inherit; }

.icon {
    width: 1em;
    height: 1em;
    vertical-align: -0.15em;
    flex-shrink: 0;
}
.icon-lg { width: 1.4em; height: 1.4em; }

/* Real emoji (not the line-art .icon set) rendered inline with text always
   looked small next to it — emoji glyphs sit lower/smaller within their own
   line-height than the icon set does. Bump and nudge them to feel intentional
   rather than like an afterthought. */
.emoji-lg {
    font-size: 1.5em;
    display: inline-block;
    vertical-align: -0.28em;
    line-height: 1;
}

/* iOS/Safari's UA stylesheet centers the date value inside a date input by
   default (its own shadow-DOM piece, not reachable via plain text-align on
   the input) — every other text field in the app is left-aligned, so this
   one stood out. */
/* Browsers size a date input's native calendar-icon widget by their own
   rules, not just padding/min-height like a text input — an explicit
   height (not just min-height) is what actually keeps it the same size
   as its siblings in the same form. */
input[type="date"] { text-align: left; height: 44px; }
input[type="date"]::-webkit-date-and-time-value { text-align: left; }

/* iOS Safari specifically (confirmed via a real-device screenshot, not
   reproducible in desktop testing): plain height:44px above wasn't
   enough — its native date-picker control keeps its own much taller
   intrinsic rendering size regardless, roughly 3x every sibling field's
   height. Stripping appearance first is what actually lets the explicit
   height take over. Scoped to form.card specifically to win the
   specificity fight against form.card input's own padding rule.
   appearance:none doesn't break tap-to-open-the-picker on a type="date"
   input the way it can on some other input types, so this shouldn't
   cost any functionality — but flagging that this couldn't be verified
   on a real device before deploying, unlike everything else that could
   be checked in a local test browser. */
form.card input[type="date"], .fld[type="date"] {
    -webkit-appearance: none;
    appearance: none;
    height: 44px;
    line-height: 44px;
    padding: 0 0.9rem;
    overflow: hidden;
}

input[type="checkbox"] {
    width: 24px;
    height: 24px;
    /* The UA default is margin: 3px 3px 3px 4px, which pushed the box 4px
       right of the column its label started in, and made every flex gap
       beside it 3px wider than the gap actually set. */
    margin: 0;
    accent-color: var(--accent);
    cursor: pointer;
    vertical-align: middle;
    flex-shrink: 0;
}

table.books th:first-child, table.books td:first-child {
    padding: 0;
}

html {
    background: var(--bg);
}

body {
    margin: 0;
    font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
    background-color: var(--bg);
    background-image: var(--bg-gradient);
    background-attachment: fixed;
    color: var(--text);
    -webkit-font-smoothing: antialiased;
}

/* Was standalone-only (display:none otherwise, shown via
   @media (display-mode: standalone)) on the reasoning that a regular browser
   tab already has its own chrome and didn't need a second one. True for the
   decorative wordmark, but the avatar/wishlist/settings icons living in this
   bar are the *only* links to /profiles and /settings anywhere in the app —
   nothing else points at them — so hiding the bar in a plain tab didn't just
   hide branding, it made Settings and profile-switching completely
   unreachable outside an installed PWA. Now unconditional; env() safe-area
   insets are already 0 in a normal tab (no notch to clear there), so nothing
   below needed to change to also run outside standalone.

   Without viewport-fit=cover + this, iOS paints the notch/status-bar strip
   and home-indicator strip plain white regardless of the page's own
   background, because that area isn't part of the layout viewport at
   all by default. A solid brand-colour block read as a loud, boxy
   banner sitting on top of the page. Tinted with the page's own
   background (not the card colour — that read as a visibly different,
   boxed-in tile) and faded out at the bottom edge instead of a hard
   border marking where the bar ends. It used to dissolve into the page via
   a mask-image instead; that read as fog over the logo rather than as
   depth, so it's a solid surface with a hairline rule now. */
.pwa-topbar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: calc(env(safe-area-inset-top) + 64px);
    /* padding-top is exactly the notch/status-bar inset, nothing more —
       .pwa-topbar-inner's align-items:center then centers the row within
       the bar's actual *visible* space below the notch, both axes, rather
       than against the whole box (which would skew it toward the bottom
       since the notch inset eats unusable space at the top). Left/right
       match .lib-hero's own 16px so the avatar/wordmark sit on the same
       left edge as the heading/subtitle in the content below, instead of
       the topbar having its own independent inset. */
    padding: env(safe-area-inset-top) 16px 10px;
    /* Solid, fully opaque. This was three stacked washing-out effects —
       an 82%-opacity background, a 20px backdrop blur, and a mask-image
       fading the bottom edge to transparent — which together left the
       avatar, wordmark and icons looking fogged in the installed PWA.
       A hairline bottom border now marks where the bar ends, which is
       the job the fade was doing badly. */
    background: var(--bg);
    border-bottom: 1px solid var(--border-soft);
    z-index: 200;
}
/* The bar itself stays full-bleed (above) so its background/border still
   spans edge to edge on a wide screen — this inner row is what actually
   caps and centers, same 1180px sitewide ceiling as <main>. Without it the
   brand sat pinned to the left edge and the icons to the right edge of the
   whole physical window, hundreds of pixels apart on a wide desktop browser
   — never visible before this bar showed outside an installed PWA (narrow
   phone screens only, where the gap this closes never had room to appear). */
.pwa-topbar-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    height: 100%;
    max-width: var(--content-max);
    margin: 0 auto;
}
/* Brand group (avatar + wordmark) sits left, actions (wish list,
   settings) sit right — not a centered wordmark. Matches a standard
   app-bar layout: identity leads, utility actions trail. */
.pwa-topbar-brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
.pwa-topbar-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.pwa-topbar-avatar, .pwa-topbar-icon-btn { flex-shrink: 0; }
/* Doubled-class selector beats .avatar-badge's own 32px on specificity
   regardless of source order — same fix as .lib-hero-icon-btn.settings-toggle
   below, for the same underlying equal-specificity conflict. */
.avatar-badge.pwa-topbar-avatar { width: 34px; height: 34px; }
/* A visible circle at rest, not just on :hover — :hover essentially
   never fires on a touchscreen, so without this the buttons were
   borderless/invisible in normal use, while the avatar right next to
   them is always a bold solid-filled circle. Same weight now, both
   always-visible circular elements, not one bold and one a ghost. */
.pwa-topbar-icon-btn {
    width: 34px;
    height: 34px;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: var(--card);
    color: var(--muted);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}
.pwa-topbar-icon-btn:hover { background: var(--border-soft); color: var(--text); }
/* At the base 1em (16px) these read as thin, sparse line-art floating
   in an otherwise-empty 34px circle — much lighter visual weight than
   the solid-filled avatar circle right next to them. Sized up so the
   icon actually reads as a comparable presence, not just a matching
   bounding box. */
.pwa-topbar-icon-btn .icon { width: 20px; height: 20px; }
.pwa-topbar-wordmark-btn {
    background: none;
    border: none;
    font-family: inherit;
    cursor: pointer;
    padding: 0;
}
/* Wordmark logo — plain "Book" + green "Trek", with a filled pin
   standing in for a full stop. Baloo 2 (loaded per-page in <head>)
   instead of the body's Inter so it reads as a distinct brand mark,
   not more UI text. Green needs its own light/dark pair (unlike the
   purple gradient elsewhere) since this bar's background is the
   page's own near-white/near-black tone, not a fixed purple card. */
.pwa-topbar-wordmark {
    display: inline-flex;
    align-items: center;
    font-family: 'Baloo 2', sans-serif;
    font-weight: 800;
    font-size: 1.5rem;
    letter-spacing: -0.02em;
    color: var(--text);
}
.pwa-topbar-trek { color: var(--brand-green); }
.pwa-topbar-pin { width: 20px; height: 20px; margin-left: 6px; fill: var(--brand-green); }
.pwa-topbar-pin circle { fill: var(--accent-dark); }
/* The brand mark was sized for a ~375px phone and never grew from there —
   on an iPad or a desktop browser window it read as tiny relative to the
   space around it. Scales up together (wordmark, pin, avatar, and the
   wishlist/settings icon buttons) rather than just the text, so the whole
   brand group grows as one unit instead of the wordmark suddenly
   outsizing the avatar next to it. Same 641px line the rest of the app
   already uses for "wide enough that phone-only choices stop applying". */
/* Must match .pwa-topbar's own height (safe-area + 64px) plus a gap.
   This was 44px, which worked only because the bar used to fade its
   bottom 20px to transparent — once it became a solid surface the
   shortfall became a hard clip across the top of the page heading.

   This rule must stay ABOVE the @media block below, not below it — a bare
   `body` selector here and the one inside the media query have identical
   (0,0,1) specificity, so whichever comes LAST in the file wins regardless
   of which media query actually matches. Had this the other way round
   once already (the wordmark-scaling block that follows) and it silently
   kept every viewport at 68px, topbar included — the topbar's own height
   still grew to 74px at 641px+, so it ended up 6px taller than the space
   reserved for it, overlapping the top of the page's own content. */
body { padding-top: calc(env(safe-area-inset-top) + 68px); }
@media (min-width: 641px) {
    .pwa-topbar-wordmark { font-size: 1.9rem; }
    .pwa-topbar-pin { width: 24px; height: 24px; margin-left: 7px; }
    .avatar-badge.pwa-topbar-avatar { width: 42px; height: 42px; }
    .pwa-topbar-icon-btn { width: 40px; height: 40px; }
    .pwa-topbar-icon-btn .icon { width: 22px; height: 22px; }
    .pwa-topbar { height: calc(env(safe-area-inset-top) + 74px); }
    body { padding-top: calc(env(safe-area-inset-top) + 78px); }
}
/* The sign-in, setup and 2FA screens print the brand name as plain text. It
   was set in Inter at one flat colour while those pages were already loading
   Baloo — the wordmark rendered as if it were body copy, on the first screen
   anyone sees. Same face and same split as .pwa-topbar-wordmark now. */
.auth-title { font-family: 'Baloo 2', sans-serif; }
/* One flex item, not two. .auth-title is display:flex with a 0.55rem gap for
   the logo, so leaving "Book" and "Trek" as separate children put that gap
   between them and the name read as two words. */
.auth-wordmark { display: inline-flex; }
.auth-trek { color: var(--brand-green); }

a { color: var(--accent); }

/* Wraps header.top using main's *exact* box formula (same max-width,
   margin:auto, and horizontal padding as <main> below) so the header card's
   edges track the chips/filters/table at every window width instead of
   drifting — main has two layers (its own centred box, then padding inside
   that), so header needed the same two layers, not just a matching
   max-width, to actually land in the same place. */
.header-align-wrap {
    max-width: var(--content-max);
    margin: 0 auto;
    padding: 0 2.5rem;
}

/* Reworked from a full-bleed solid purple gradient card to a plain header
   on the page's own neutral canvas — that gradient-card-as-page-header
   pattern reads as generic/template-y at this point, and purple works
   harder as a small accent (pill, panel, badge) than as the dominant
   surface of the whole page. padding-top still matches trail.css's own
   22px so "Catrine's Library" keeps landing on the same line as "Catrine's
   Reading Trail" on the trail page, which still uses its purple hero. */
/* Horizontal padding is deliberately small (2px, not a round number) —
   .header-align-wrap already contributes 14px of its own on this side
   (mirroring <main>'s box model so the header tracks the content below
   it), and 14+2=16 is what lands the heading on the same left edge as
   the pwa-topbar and the trail pages (both a flat 16px, no separate
   wrapper layer in between). Using a "nicer" 16px here double-counted
   the wrapper's own inset and pushed the heading 14px further right than
   everything else on the page. Top padding is a separate, purely
   vertical-rhythm choice, unrelated to this. */
/* Zero horizontal padding: .header-align-wrap already supplies the page's
   16px, and the 2px this used to add on top put the hero's edges 2px inside
   the search bar and book cards below it. Everything now shares one edge.
   Top margin is 2px rather than 10 — under the fixed topbar that 10px was
   stacking with the body's own clearance into a visible gap. */
/* Bottom padding is 2px, not 12: <main> below contributes its own 12px of
   top padding, and the two were stacking into a 24px trough between the
   stats card and the search bar. 2+12 lands on the intended 14. */
.lib-hero {
    padding: 10px 0 2px;
    margin: 2px 0 0;
}
/* Heading and the read-ring share one row (space-between) rather than the
   ring sitting alone with no relationship to anything near it. Centered,
   not flex-start — unlike Trail's profile-name heading (which can wrap to
   two lines and needs flex-start to keep the ring pinned to the first
   line), "Family Library" is fixed, short text that never wraps, so
   flex-start here only left the ring's circle — which starts exactly at
   the row's top edge — sitting optically higher than the heading text,
   which has its own line-height leading above the actual glyphs. */
.lib-hero-heading-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}
/* Baloo 2 at 1.5em, matching .trail-name on Trail, Goals and Board. The four
   tab-bar destinations share this heading; the cog-menu pages (Settings,
   Stats, Wish List) use the smaller Inter .page-head h1. Briefly changed to
   Inter to match those, which was the wrong comparison — it made the library
   the odd one out among its own siblings. */
.lib-hero-heading {
    font-family: 'Baloo 2', sans-serif;
    font-weight: 800;
    font-size: 1.5em;
    letter-spacing: -0.02em;
    color: var(--text);
    display: flex;
    align-items: center;
    gap: 10px;
}
/* Read/unread progress ring — a small donut chart rather than another flat
   number pill, so "how much of the library has actually been read" reads
   as a fill level at a glance instead of two numbers you have to do the
   division on yourself. Lives here (not on Trail) because book status —
   what the ring measures — is shared library data, not per-profile trail
   progress: the library itself is one shelf the whole family reads from,
   while the trail is each profile's own individual pace through it. */
.read-ring {
    position: relative;
    /* Draws at 48px but only occupies 38px of layout, via the negative
       margins — the heading text sets this row at 39px, so anything up to
       that costs nothing. Sizing it plainly at 46px is what made it the
       tallest thing here and pushed the title out of line with the other
       tab pages. */
    width: 48px;
    height: 48px;
    /* Asymmetric on purpose: -8/-2 still leaves 38px of layout, exactly as
       -5/-5 did, so the row height is unchanged — but it sits the circle 3px
       higher. Even margins left 7.3px above it and 1.3px below, so it very
       nearly touched the subtitle; it's ~4.3px both sides now. */
    margin: -8px 0 -2px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    cursor: pointer;
}
.read-ring:hover .read-ring-bg { stroke: var(--border); }
.read-ring-svg {
    width: 100%;
    height: 100%;
    transform: rotate(-90deg);
}
.read-ring-bg {
    fill: none;
    stroke: var(--border-soft);
    stroke-width: 4;
}
.read-ring-fg {
    fill: none;
    stroke: var(--read);
    stroke-width: 4;
    stroke-linecap: round;
    stroke-dasharray: 113.1;
    stroke-dashoffset: 113.1;
}
.read-ring-label {
    position: absolute;
    /* "1/647" is the longest this gets. */
    font-size: 0.66rem;
    font-weight: 800;
    color: var(--text);
    letter-spacing: -0.02em;
}
/* The white stroke on each spine was only needed to lift the logo off the
   old purple gradient — on the plain neutral canvas its own purple/blue/
   green already has plenty of contrast, so the outline (and the badge
   before it) is gone entirely now. */
/* Fixed 36px rather than 1.3em: it has to occupy exactly the same footprint
   as the icon on the other tab pages for the headings to line up, and
   em-sizing tied it to this page's heading font instead.
   The 2px lift is optical, not geometric — the artwork's centre is already
   within a pixel of the text's, but the solid shelf bar under the spines
   carries visual weight low, so a box-centred logo reads as sitting low.
   position:relative keeps it out of the layout, so the row is untouched. */
.lib-hero-heading-logo {
    width: 36px;
    height: 36px;
    flex-shrink: 0;
    position: relative;
    top: -2px;
}
/* The one line of copy under a page's heading, wherever a page has one:
   Trail, Goals, Board, Library, Stats, Wish List and Switch Profile.
   Each of those used to carry its own rule and they had drifted to three
   different sizes (0.78/0.82/0.85), two different margins, and a
   line-height set on only some of them.

   rem, not em: at 0.82em the size depended on whatever the surrounding
   header happened to set, which is how they came apart in the first place.

   No text-wrap: balance. It was on the Trail/Goals/Board copy only, and
   balancing is what stopped the text reaching the full width — it evens
   the lines out instead of filling the first one. */
.page-sub {
    color: var(--sub);
    /* 1rem, not 0.82rem. The heading above is Baloo 800; 13px grey beneath it
       reads as a caption, because small and pale is the typographic signal for
       fine print. At 16px it becomes a deck — part of the heading block rather
       than a footnote to it. */
    font-size: 1rem;
    line-height: 1.45;
    /* Larger type wants slightly less tracking to stay tight. */
    letter-spacing: -0.011em;
    margin: 12px 0 0;
    /* A measure, so the line can't run the full width of a desktop window —
       still doesn't bind on mobile, same as before. Was 48ch, picked before
       any page was ever wide enough for it to actually bind against real
       copy; once pages reached desktop width it turned out shorter than the
       page's own subtitle sentences ("Every page takes you further — Land's
       End to John o'Groats on foot." is ~71 characters), wrapping them
       awkwardly mid-thought for no typographic reason. 70ch fits the
       one-line subtitles comfortably; a genuinely long one (Wish List's) still
       wraps, which is fine at that length. */
    max-width: 70ch;
}
/* The half of the sentence that carries the payoff, lifted to full-strength
   ink. It echoes the wordmark's two-tone split without spending a colour —
   green means "finished" and purple means "active", so neither is free.

   <b> rather than <strong>: this is stylistic offset for hierarchy, not
   importance, and a screen reader shouldn't announce it as emphasis. */
/* The household's pooled week, shown only in "pulling together" mode. */
.lb-family {
    padding: 14px 2px 16px;
    border-bottom: 1px solid var(--border-soft);
    margin-bottom: 6px;
}
.lb-family-km {
    font-family: 'Baloo 2', sans-serif;
    font-size: 2.3rem; font-weight: 800; letter-spacing: -0.02em;
    line-height: 1.1; color: var(--text);
}
.lb-family-sub { font-size: 0.86rem; color: var(--muted); margin-top: 3px; }

/* When the week began, stated once, quietly. */
.lb-since {
    font-size: 0.76rem;
    color: var(--muted);
    padding: 2px 2px 10px;
}

/* A bar against the week's leader, so first place is full and the rest read
   as a share of the pace being set. */
/* The stats line and the bar must not wrap: two-line rows made the list
   ragged and pushed the percentage out of line with the name it belongs to. */
.lb-info { min-width: 0; }
.lb-name, .lb-stats { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lb-share {
    height: 4px; border-radius: 99px; background: var(--border-soft);
    margin-top: 5px; overflow: hidden;
}
/* Nobody has read yet, so there is no rank, no share and no bar — just a
   name, set back so the list still reads as "these people, quietly". */
.lb-row-idle { opacity: 0.62; }
.lb-row-idle .lb-stats { font-style: normal; }
.lb-share span { display: block; height: 100%; background: var(--accent); border-radius: 99px; }
.lb-pct {
    font-size: 0.8rem; font-weight: 700; color: var(--accent-dark);
    font-variant-numeric: tabular-nums; flex-shrink: 0;
    /* Aligned to the name, not the middle of a three-line block. */
    align-self: flex-start; padding-top: 2px;
}

/* A small standing fact under the deck — when something resets, what a page
   is scoped to. Quieter than the deck above it, and set apart by the icon
   rather than by a box, which would compete with the cards below. */
.page-note {
    display: flex;
    align-items: center;
    /* The sentence is wrapped in a single <span> so this gap sits between the
       icon and the text only. Left as separate children, the emphasised day
       became its own flex item and the gap opened up before the full stop —
       "Monday ." — the same trap as the sign-in wordmark. */
    gap: 6px;
    margin: 8px 0 0;
    font-size: 0.8rem;
    color: var(--muted);
}
.page-note b { color: var(--sub); font-weight: 600; }
.page-note .icon { width: 14px; height: 14px; flex-shrink: 0; }

/* ISBN field with its scan button beside it, on the edit form. */
.isbn-row { display: flex; align-items: center; gap: 8px; }
.isbn-row .fld { flex: 1; min-width: 0; }
.isbn-scan {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 44px;
    padding: 0 14px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--card);
    color: var(--text);
    font: inherit;
    font-size: 0.86rem;
    font-weight: 600;
    cursor: pointer;
}
.isbn-scan:active { background: var(--border-soft); }
.isbn-scan .icon { width: 15px; height: 15px; }
.isbn-hint { margin: 6px 0 0; font-size: 0.76rem; color: var(--muted); }

/* Desktop-only, same reasoning as table.books td:first-child above: fixing a
   bad cover means hunting down a direct image URL elsewhere and pasting it
   in, a fiddly task that doesn't belong on the cramped mobile form. */
.cover-url-field { display: none; }
@media (min-width: 641px) {
    .cover-url-field { display: block; }
}
.cover-url-row { display: flex; align-items: center; gap: 10px; }
.cover-url-row .fld { flex: 1; min-width: 0; }

/* The two household preference rows on Settings. */
.pref-form { display: flex; flex-direction: column; }
.pref-select {
    flex-shrink: 0;
    min-height: 44px;
    padding: 8px 10px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--card);
    color: var(--text);
    font: inherit;
    font-size: 0.88rem;
}
/* Inline beside the label, not stacked underneath: these are settings you
   touch once, so they shouldn't take a full row each. One fixed width for
   all of them so their edges line up — letting each size to its own value
   was what made the column look ragged. Option wording is kept short enough
   to fit it. */
.pref-form .sec-row { align-items: center; }
.pref-form .pref-select {
    flex: 0 0 8.5rem;
    width: 8.5rem;
}

/* Confirmation in place of a Save button — these apply as you pick them. */
.pref-saved {
    display: block;
    min-height: 1.1rem;
    padding: 6px 2px 0;
    text-align: right;
    font-size: 0.76rem;
    font-weight: 600;
    color: var(--ok-fg);
}
.page-sub b {
    color: var(--text);
    font-weight: 600;
}
/* A .page-head row is 36px tall because of the back button, while the h1's
   own box is ~24px — so ~10px of that row already sits under the heading
   text. The margin stacks on top of it and opens a 20px gap where Trail,
   Goals, Board and Library all show 10px. Measured from the heading text,
   this lands all six within 0.2px of each other. */
.page-head + .page-sub { margin-top: 0; }
/* One small soft-tint panel instead of a shelf-value box, a separate
   format-mix bar, and a latest-book line stacked as three different
   elements — combines total value and unread backlog into one tappable
   surface. The bar segments are reading STATUS (read/reading/unread),
   not format mix, since that's the number that actually reflects the
   app's job of getting you through books you already own, and it reuses
   the same colours as the status pills elsewhere instead of a separate
   palette. */
/* Pulled out to the hero's full outer width via negative margin matching
   .lib-hero's own horizontal padding — without this the panel sat inset
   on both sides (padding applies to it same as the heading/subtitle text
   above), while the cards below it start flush with the shared edge, so
   the one visible box in the hero read as narrower than everything below
   it instead of lining up. */
.lib-hero-stats {
    display: block;
    /* White card on the neutral page rather than a lavender panel — the tint
       was doing the work that a border and a shadow should do. */
    background: var(--card);
    border: 1px solid var(--border-soft);
    box-shadow: var(--shadow-sm);
    border-radius: 16px;
    padding: 16px;
    /* 15px below the subtitle, which is what the trail pages land on — see
       .header-no-stats in trail.css. This was 8px. */
    margin: 15px 0 0;
    text-decoration: none;
    transition: box-shadow 0.12s ease;
}
.lib-hero-stats:hover { background: color-mix(in srgb, var(--accent-soft) 70%, var(--accent) 8%); }
.lib-hero-stats-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.lib-hero-stats-amt { font-size: 0.82em; font-weight: 700; color: var(--accent-dark); }
.lib-hero-stats-amt span { font-weight: 400; color: var(--muted); font-size: 0.9em; }
.lib-hero-progress { display: flex; height: 10px; border-radius: 999px; overflow: hidden; margin-top: 14px; background: var(--border-soft); gap: 2px; }
.lib-hero-progress-seg { border-radius: 999px; }
.lib-hero-progress-seg { flex-grow: 0; flex-basis: 0; }
/* Coloured directly rather than via --read/--reading/--unread: those tokens
   are warm tones shared with the nudge banner and the trail's own pills, and
   retuning them for this bar would have dragged those along with it. These
   match the status badges instead. */
.lib-hero-progress-read { background: var(--info-fg); }
.lib-hero-progress-reading { background: var(--ok-fg); }
.lib-hero-progress-unread { background: var(--border); }
.lib-hero-latest {
    font-size: 0.76em;
    color: var(--muted);
    margin-top: 12px;
    line-height: 1.4;
}
.lib-hero-latest .icon {
    display: inline-flex;
    width: 12px;
    height: 12px;
    vertical-align: -0.15em;
    margin: 0 1px;
}

header.top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    /* 2px, not a round number — .header-align-wrap already contributes
       14px of its own here, and 14+2=16 matches .lib-hero/.header/
       .pwa-topbar's flat 16px (see .lib-hero's own comment for the full
       reasoning). The old 2rem here, then a since-corrected-but-still-
       wrong 16px, both put stats/wishlist/tips/settings well right of
       everything else on the page. Top/bottom padding (22px/1.15rem, not
       matching each other) mirrors .header/.lib-hero's own top/bottom
       split, so the heading lands at the same vertical position too. */
    /* Top padding and margin tightened to match .lib-hero and trail's
       .header — 14px margin over 22px padding put 36px above the heading on
       every one of these pages. */
    padding: 10px 2px 0.9rem;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 16px;
    margin: 2px 0 0;
    box-shadow: var(--shadow-sm);
}

header.top h1 {
    font-size: 1.6rem;
    font-weight: 800;
    letter-spacing: -0.03em;
    margin: 0;
    display: flex;
    align-items: center;
    gap: 0.55rem;
    white-space: nowrap;
    flex-shrink: 0;
    background: linear-gradient(135deg, var(--text) 35%, var(--accent-dark) 130%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

header.top h1 img.logo {
    height: 1.85rem;
    width: 1.85rem;
}

.avatar-badge {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: var(--accent);
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 0.85rem;
    text-decoration: none;
    flex-shrink: 0;
    box-shadow: var(--shadow-sm);
    transition: box-shadow 0.12s ease, transform 0.12s ease;
}
.avatar-badge:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); }

header.top .actions {
    display: flex;
    gap: 0.6rem;
    flex-wrap: wrap;
    justify-content: flex-end;
}

.btn-primary, .btn-secondary {
    text-decoration: none;
    padding: 0.7rem 1.3rem;
    border-radius: 10px;
    font-size: 0.92rem;
    font-weight: 600;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4em;
    font-family: inherit;
    cursor: pointer;
    transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
}

.btn-primary {
    background: linear-gradient(135deg, var(--accent), var(--accent-dark));
    color: white;
    border: none;
    box-shadow: var(--shadow-sm);
}

.btn-primary:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); }

.btn-secondary {
    background: var(--card);
    color: var(--text);
    border: 1px solid var(--border);
}

.btn-secondary:hover { border-color: var(--accent); color: var(--accent); }

.hidden { display: none !important; }

.settings-toggle {
    padding: 0;
    width: 44px;
    flex-shrink: 0;
}
/* .settings-toggle's width:44px (above) beats .pwa-topbar-icon-btn's own
   34px on the topbar's settings button — same specificity, and this rule
   comes later in the file, so width would win here while height stayed
   at 34px, making a 44x34 oval instead of a circle. The doubled-class
   selector's higher specificity fixes it regardless of source order. */
.pwa-topbar-icon-btn.settings-toggle {
    width: 34px;
    height: 34px;
    padding: 0;
}


.gear-icon {
    width: 20px;
    height: 20px;
}

/* Fixed, not absolute — this is only ever triggered from the topbar's
   settings button now (pwa-topbar, standalone-PWA-only), and .settings-menu
   is a sibling of .pwa-topbar in the DOM, not nested inside it. That began
   as a workaround — the topbar used to carry a mask-image fading its bottom
   edge, which applied to everything painted inside it, so a nested menu
   toggled open correctly but rendered fully invisible. The mask is gone now,
   but the menu stays a sibling: it hangs well below the bar's 64px height
   and nesting it would re-introduce a clipping relationship for no gain.
   Coordinates only make sense in standalone mode, the only place this
   class's trigger button is ever reachable. */
.settings-menu {
    position: fixed;
    top: calc(env(safe-area-inset-top) + 64px + 6px);
    right: 16px;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 12px;
    box-shadow: var(--shadow-md);
    padding: 0.4rem;
    display: flex;
    flex-direction: column;
    min-width: 180px;
    z-index: 250;
}

.settings-menu a, .settings-menu button {
    /* Flex, not block. The icons were inline, so they sat on the text
       baseline rather than centred against it, and the space between icon and
       label was a literal space character in the markup — which is why the
       rows never quite lined up. */
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    text-align: left;
    padding: 0.65rem 0.8rem;
    border-radius: 8px;
    border: none;
    background: none;
    color: var(--text);
    text-decoration: none;
    font-size: 0.9rem;
    font-family: inherit;
    cursor: pointer;
    min-height: 40px;
}

.settings-menu a:hover, .settings-menu button:hover { background: var(--border-soft); }
.settings-menu .icon { width: 16px; height: 16px; flex-shrink: 0; color: var(--muted); }
.settings-menu form { margin: 0; }
/* Leaving is a different kind of act from navigating, so it sits below a
   rule rather than in the list. Not red: nothing is lost by signing out, and
   red in this app already means Delete — a book, a profile — so spending it
   on a reversible action would make the genuinely destructive ones quieter. */
.settings-menu .menu-sep {
    height: 1px;
    margin: 0.35rem 0.2rem;
    background: var(--border);
}

main {
    max-width: var(--content-max);
    margin: 0 auto;
    /* Bottom padding clears the fixed .tab-nav bar, sitewide (it's
       position:fixed on every page/breakpoint now, not just mobile) — see
       the .tab-nav rule below. The `main` rule just below overrides this
       bottom value; Trail zeroes it entirely in main.trail-main and puts the
       padding on .page instead. */
    padding: 1rem 2.5rem 110px;
}
/* Clears the docked .tab-nav (64px) and the 12px the [+] rises above its top
   border, plus a gap — 76px is the floor, 82 leaves ~16px clear of the
   button. 100px was over-generous and left a visible band of empty page above
   the bar once you'd scrolled to the end of the list. */
main { padding-bottom: calc(82px + env(safe-area-inset-bottom)); }

.summary {
    display: flex;
    gap: 0.6rem;
    flex-wrap: wrap;
    margin-bottom: 0.85rem;
}

.summary .chip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    /* Flat: no border, no shadow. Bordered-and-shadowed pills read as raised
       buttons, which fought the cards below them for prominence. */
    background: var(--border-soft);
    border: none;
    border-radius: 999px;
    padding: 0.45rem 1rem;
    font-family: inherit;
    font-size: 0.83rem;
    font-weight: 600;
    color: var(--sub);
    text-decoration: none;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}
.summary .chip:hover { background: var(--border); }
.summary .chip.active {
    background: var(--accent);
    color: #fff;
}
.summary .chip.active:hover { background: var(--accent-dark); }

.summary .chip strong { color: var(--text); font-weight: 700; }
.summary .chip.active strong { color: #fff; }

.chip-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    border-radius: 6px;
    background: var(--amazon);
    box-shadow: var(--shadow-sm);
    color: white;
    vertical-align: -0.35em;
    flex-shrink: 0;
}
.chip-icon .icon { width: 12px; height: 12px; }

.bulk-bar {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
    margin-bottom: 0.9rem;
    padding: 0.7rem 0.9rem;
    background: var(--accent-soft);
    border: 1px solid var(--border);
    border-radius: 14px;
}

.bulk-bar-controls {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.6rem;
}

.bulk-bar select, .bulk-bar input[type="text"] {
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--card);
    color: var(--text);
    font-size: 0.92rem;
    height: 44px;
}

.bulk-bar input[type="text"] { padding: 0 0.9rem; }

.bulk-bar select {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    padding: 0 2.1rem 0 0.9rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%2364748b' stroke-width='1.6'%3E%3Cpath d='M5 7l5 5 5-5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.75rem center;
    background-size: 14px;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme='light']) .bulk-bar select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%239c9389' stroke-width='1.6'%3E%3Cpath d='M5 7l5 5 5-5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
}
:root[data-theme='dark'] .bulk-bar select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%239c9389' stroke-width='1.6'%3E%3Cpath d='M5 7l5 5 5-5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }


#bulk-count {
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--accent-dark);
}

/* Read-only card list, unified across desktop and mobile — replaces what
   used to be a real HTML table on desktop (column headers, one field per
   column) that reflowed into stacked "cards" via a page of order: hacks on
   narrow screens. Now that the row has no inline selects left to line up
   in columns, there's no reason to maintain two different layouts: every
   book is a self-contained tappable card at any width, just with a bit
   more breathing room on wide screens. */
table.books {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    background: none;
    box-shadow: none;
    border: none;
}
table.books thead { display: none; }
table.books, table.books tbody, table.books tr, table.books td {
    display: block;
    width: auto;
}
table.books tbody tr.book-row {
    display: flex;
    align-items: flex-start;
    position: relative;
    padding: 14px;
    margin-bottom: 12px;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 16px;
    box-shadow: var(--shadow-sm);
    cursor: pointer;
    transition: background 0.1s ease;
    scroll-margin-top: 90px; /* clears the sticky header when jumped to via #book-<id> */
}
/* [hidden] needs to be reasserted at this specificity or it does nothing.
   The rule above sets display:flex at (0,1,3); the browser's own
   `[hidden] { display: none }` is (0,0,0) and loses, so live search could set
   the property on a row and the row would stay on screen regardless. */
table.books tbody tr.book-row[hidden] { display: none; }
table.books tr:last-child { margin-bottom: 0; }
table.books tbody tr.book-row:hover { background: var(--border-soft); }
table.books td { padding: 0; border-bottom: none; text-align: left !important; }

/* Bulk-select is desktop-only in practice (bulk actions are only ever done
   there) — hidden entirely on narrow screens rather than just visually,
   since there's no other way to select a book to act on without it.
   Scoped to tr.book-row specifically: a series-stack or ghost-vol row has
   only one <td> total, which makes it :first-child too — the unscoped
   selector used to match it, absolutely-positioning its only content box
   and collapsing the row to near-zero height (found while adding the wide
   grid below, which made a 2px-tall series card impossible to miss).
   Within that desktop width, the checkbox column is a mode now (see
   #select-mode-toggle in app.js) rather than always on — it used to just
   appear at 641px, leading with "select some books" on every visit. */
table.books tbody tr.book-row td:first-child { display: none; }
@media (min-width: 641px) {
    body.select-mode table.books tbody tr.book-row td:first-child {
        display: block;
        position: absolute;
        top: 16px;
        left: 14px;
    }
    body.select-mode .title-cell { padding-left: 26px !important; }
}

.title-cell { flex: 1; min-width: 0; }
.title-row { display: flex; align-items: flex-start; gap: 12px; }
.title-text { flex: 1; min-width: 0; }

.cover-wrap { position: relative; flex-shrink: 0; }
.cover-thumb {
    display: block;
    /* Fixed box + object-fit:cover. Covers come back from Google at wildly
       different aspect ratios, so anything self-sizing made a column of these
       look ragged. The inset ring stops a white cover bleeding into a white
       card without adding an outer border that would read as a frame. */
    width: 56px;
    height: 80px;
    object-fit: cover;
    border-radius: 6px;
    box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.08), var(--shadow-sm);
    background: var(--border-soft);
}

.cover-thumb-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.3rem;
    color: var(--muted);
}
/* Small avatar chip on the cover's corner for who the book's for — moved
   off the metadata row entirely (it was crowding date/price/format/status
   into five competing elements), same idea as a shared-doc avatar. */

.book-line1 { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
/* A real, independent control — its own small dropdown (Edit full
   details / Get info / Delete), separate from tapping the rest of the
   card (which opens the quick-edit sheet instead). A normal-sized 28px
   visible button so it doesn't blow out the row's height next to a short
   single-line title, with a separate absolutely-positioned ::before
   extending its actual tappable area to 44x44 — a plain negative margin
   on the button doesn't reliably hit-test the same as its visual box on a
   flex item (confirmed via elementFromPoint: the edge of a margin-
   expanded box was resolving to the row underneath, not the button).
   ::before's own explicit position:absolute box doesn't have that
   problem. */
.row-menu { position: relative; display: inline-flex; }
.book-more {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    /* 30x30 to match .quick-toggle beside it. These two sit in the same row
       and were a 2px different box, so their hover backgrounds and pressed
       states sat fractionally out of line with each other. */
    width: 30px;
    height: 30px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: none;
    color: var(--muted);
    font-size: 1.05rem;
    flex-shrink: 0;
    cursor: pointer;
    transition: background 0.12s ease, color 0.12s ease;
}
.book-more::before {
    content: "";
    position: absolute;
    /* Vertical only, near enough. A 44px-square target on both would put 7px
       of each into the 6px gap between them, leaving an 8px band that could
       fire either control. 3px a side exactly meets its neighbour without
       crossing it: 44px tall, 36px wide, no ambiguity. */
    inset: -7px -3px;
}
.book-more:hover { background: var(--border-soft); color: var(--text); }
.book-more:active { background: var(--border); transform: scale(0.94); }
.row-menu-list {
    position: absolute;
    top: calc(100% + 4px);
    right: 0;
    /* Never taller than the space it's given — a five-item menu on a short
       screen scrolls inside itself rather than running off the page. */
    max-height: 60vh;
    overflow-y: auto;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 12px;
    box-shadow: var(--shadow-md);
    padding: 0.4rem;
    display: flex;
    flex-direction: column;
    min-width: 170px;
    z-index: 20;
    white-space: nowrap;
}
/* Opens upward when there isn't room below. The tab bar is fixed, so the
   usable floor is its top edge, not the bottom of the viewport — without this
   a menu on one of the last rows opened underneath it. */
.row-menu-list.up { top: auto; bottom: calc(100% + 4px); }
.row-menu-list a, .row-menu-list .link-button {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    text-align: left;
    padding: 0.6rem 0.7rem;
    border-radius: 8px;
    color: var(--text);
    font-size: 0.85rem;
    min-height: 38px;
    margin: 0;
    text-decoration: none;
}
/* Doubled selector so this reliably beats the generic .link-button:hover
   rule elsewhere (same specificity otherwise, order-dependent) rather
   than relying on where each happens to land in the file. */
.row-menu .row-menu-list a:hover, .row-menu .row-menu-list .link-button:hover { background: var(--border-soft); color: var(--text); }
.row-menu-delete { color: #d9534f; }
.book-line1 strong {
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--text);
    line-height: 1.35;
    /* One line then ellipsis. Two lines let a long title push the metadata
       rows out of alignment with the cards around it. */
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
    min-width: 0;
}
.book-line2 {
    font-size: 0.82rem;
    color: var(--muted);
    margin-top: 0;
    /* Single line — author plus series plus rating can run long, and wrapping
       it pushed the metadata row out of line with the cards around it. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Not space-between: with three children it spread them evenly and parked
   the Sample pill midway across, away from the format it qualifies. The
   status badge is pushed right on its own instead, so format and Sample stay
   together on the left. With no Sample pill the result is identical to before. */
.book-line3 { display: flex; align-items: center; gap: 6px; margin-top: 10px; }
.book-line3 .status-badge { margin-left: auto; }
.book-meta { display: flex; align-items: center; gap: 6px; font-size: 0.76rem; color: var(--muted); }
.book-date { white-space: nowrap; }
.book-badges { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }

.badge {
    display: inline-block;
    padding: 0.25rem 0.7rem;
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.01em;
    color: white;
    box-shadow: var(--shadow-sm);
}

.badge.amazon { background: var(--amazon); }
.badge.kindle { background: var(--kindle); }
.badge.audible { background: var(--audible); }
.badge.waterstones { background: var(--waterstones); }
.badge.other { background: var(--other); }

/* Explicit centering + tight padding, rather than relying only on the
   generic nth-child rule — this column is narrow and icon-only, so the
   default text padding leaves almost no room either side to actually
   show a centered effect. */
/* Small generic line-icons — the shape shows the format (book / headphones
   / e-reader), the colour shows the source (still orange for Amazon etc.),
   so one small icon carries two pieces of information. Not the actual
   brand logos (reproducing those needs the real assets and a licensing
   call, not something to just fetch from the web) but recognisable at a
   glance without a text pill. Read-only now (a plain span, not a select —
   format changes happen in the edit sheet instead), so just the shape/
   colour/size rules survive here. */
.source-icon {
    display: inline-block;
    position: relative;
    vertical-align: middle;
    width: 26px;
    height: 26px;
    border-radius: 7px;
    background-repeat: no-repeat;
    background-position: center;
    background-size: 55%;
}
.fmt-badge.source-icon { width: 20px; height: 20px; border-radius: 6px; flex-shrink: 0; }

/* Colour: source */
.source-icon.amazon { background-color: var(--amazon); }
.source-icon.audible { background-color: var(--audible); }
.source-icon.waterstones { background-color: var(--waterstones); }
.source-icon.other { background-color: var(--other); }

/* Shape: format */
.source-icon.fmt-audiobook {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 18v-6a9 9 0 0118 0v6'/%3E%3Cpath d='M21 19a2 2 0 01-2 2h-1a2 2 0 01-2-2v-3a2 2 0 012-2h3zM3 19a2 2 0 002 2h1a2 2 0 002-2v-3a2 2 0 00-2-2H3z'/%3E%3C/svg%3E");
}
.source-icon.fmt-ebook {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='5' y='2' width='14' height='20' rx='2' ry='2'/%3E%3Cline x1='12' y1='18' x2='12.01' y2='18'/%3E%3C/svg%3E");
}
.source-icon.fmt-physical, .source-icon.fmt-sample {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z'/%3E%3Cpath d='M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z'/%3E%3C/svg%3E");
}

/* Sample indicator — a small dot in the app's existing "sample" purple
   (same colour already used for sample status pills elsewhere), so it
   reads as a deliberate design choice rather than a random flag. */
.source-icon.is-sample::after {
    content: "";
    position: absolute;
    top: -3px;
    right: -3px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--sample);
    border: 2px solid var(--card);
}

/* Read-only pill on the card (colour still carries the state at a glance);
   the editable version lives in the sheet as a plain .sheet-select instead
   of a coloured control. */
.status-badge {
    display: inline-flex;
    align-items: center;
    min-height: 20px;   /* matches .fmt-pill next to it */
    border-radius: 6px;
    padding: 3px 8px;
    font-size: 0.68rem;
    font-weight: 600;
    white-space: nowrap;
    flex-shrink: 0;
    border: none;
    font-family: inherit;
    cursor: pointer;
    transition: transform 0.1s ease, filter 0.12s ease;
}
/* Drawn at 20px tall, which is small for a control that changes reading
   state. The whole card opens the detail sheet and this badge is excluded
   from that (see app.js), so every pixel added here is taken from "open this
   book" — hence the uneven insets. Down and out into the card's dead padding
   is cheap; up is capped at 7px so the target never covers the metadata line,
   where a tap should still open the book. */
.status-badge { position: relative; }
.status-badge::before { content: ""; position: absolute; inset: -7px -10px -12px -10px; }
.status-badge:hover { filter: brightness(0.96); }
.status-badge:active { transform: scale(0.95); }
/* Pastel fill with darker text of the same hue, rather than white on a solid
   block — four saturated blocks per row was the loudest thing on the page.
   None of them use violet: that's reserved for active state. */
.status-badge.unread  { background: var(--border-soft); color: var(--sub); }
.status-badge.reading { background: var(--warn-bg); color: var(--warn-fg); }
.status-badge.read    { background: var(--ok-bg); color: var(--ok-fg); }
.status-badge.sample  { background: var(--info-bg); color: var(--info-fg); }
.link-button {
    background: none;
    border: none;
    padding: 0;
    font-family: inherit;
    cursor: pointer;
    font-size: 0.8rem;
    color: var(--muted);
    text-decoration: none;
    transition: color 0.1s ease;
}
.link-button:hover { color: var(--accent); }

.minutes-note {
    font-size: 0.72rem;
    color: var(--muted);
    margin-top: 0.35rem;
    white-space: nowrap;
}

/* Everything below used to reflow the old table into stacked cards on
   narrow screens (series select, blurb disclosure, and a page of order:
   hacks) — all superseded now that the card is unified and read-only at
   every width and the blurb/series live in the edit sheet instead. */
/* Neutral chip (not a loud coloured pill like the status badge) — matches
   the badge language used elsewhere on the card instead of a raw caps
   label. Used at every width now, not just mobile. */
.price-amount {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    /* Plain text. The grey pill made a routine number look like a badge, and
       muddied the row it sat in. */
    font-weight: 500;
    font-size: 0.75rem;
    color: var(--muted);
}
.price-amount .price-icon { width: 12px; height: 12px; color: var(--muted); flex-shrink: 0; }

.bulk-btn:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

.empty-state {
    text-align: center;
    padding: 5rem 1rem;
    color: var(--muted);
    background: var(--card);
    border: 1px dashed var(--border);
    border-radius: 16px;
}

.empty-state p:first-child {
    font-size: 1.1rem;
    font-weight: 600;
    color: var(--text);
}

.scan-card {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    flex-wrap: wrap;
    max-width: 560px;
    margin-bottom: 1rem;
    padding: 1rem 1.25rem;
    background: var(--card);
    border: 1px dashed var(--border);
    border-radius: 12px;
}
.scan-status { color: var(--muted); font-size: 0.88rem; }
.scan-duplicate {
    max-width: 560px;
    margin-bottom: 1rem;
    padding: 0.75rem 1rem;
    background: color-mix(in srgb, var(--sample) 15%, var(--card));
    border: 1px solid color-mix(in srgb, var(--sample) 45%, var(--card));
    border-radius: 10px;
    font-size: 0.88rem;
}
.scan-duplicate.hidden { display: none; }

form.card {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 16px;
    padding: 1.75rem;
    max-width: 560px;
    box-shadow: var(--shadow-md);
}

form.card label {
    display: block;
    font-size: 0.83rem;
    font-weight: 600;
    margin: 1rem 0 0.35rem;
    color: var(--muted);
}

/* Groups related fields (reading progress; purchase info) without boxing
   them in a nested card — a top border + small caption reads as "new
   section starts here" without adding another layer of visual weight on
   top of the outer card. */
form.card .field-group-label {
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--accent-dark);
    margin-top: 1.75rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--border);
}

/* Read-only handover log under "For" — small and out of the way, since
   it's context, not something you edit directly here (editing "For" and
   saving is what adds a new row). */
.checkout-history {
    margin-top: 0.6rem;
    padding: 0.6rem 0.85rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 9px;
}
.checkout-history-row {
    display: flex;
    justify-content: space-between;
    gap: 0.75rem;
    font-size: 0.78rem;
    padding: 0.2rem 0;
    color: var(--text);
}
.checkout-history-row span {
    color: var(--muted);
    white-space: nowrap;
}

/* Fixing a typo in a shared series name — deliberately separate from the
   Series field itself, which picks/creates a series for *this* book;
   without a distinct control, editing that field's text is ambiguous
   between "rename the series everywhere" and "move just this book to a
   different one." */
.series-rename-row {
    margin-top: 0.6rem;
    padding: 0.6rem 0.85rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 9px;
    font-size: 0.78rem;
    color: var(--muted);
}
.series-rename-controls {
    display: flex;
    gap: 0.5rem;
    margin-top: 0.4rem;
}
.series-rename-controls input {
    flex: 1;
    min-height: 38px;
    padding: 0.5rem 0.7rem;
    font-size: 0.85rem;
}
.series-rename-controls .link-button {
    flex-shrink: 0;
    font-size: 0.82rem;
}
.series-rename-row .scan-status {
    display: block;
    margin-top: 0.35rem;
    font-size: 0.76rem;
}

.series-position-row.hidden { display: none; }
.series-position-row input { max-width: 160px; }

.series-link-section {
    margin-top: 0.6rem;
    padding: 0.75rem 0.85rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 9px;
}
.series-link-section.hidden { display: none; }
.series-link-section label {
    margin: 0 0 0.5rem;
    font-size: 0.78rem;
}
.series-book-search {
    width: 100%;
    padding: 0.55rem 0.8rem;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--card);
    color: var(--text);
    font-size: 0.88rem;
    font-family: inherit;
    margin-bottom: 0.5rem;
}
/* A bounded, scrollable list of one book per row. Several hundred titles as
   inline labels is a wall of text you can't pick anything out of — which is
   what this became when it lost its styling. */
.series-book-list {
    max-height: 220px;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    display: flex;
    flex-direction: column;
    gap: 2px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--bg);
    padding: 6px;
}
.series-book-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 8px;
    font-size: 0.85rem;
    font-weight: 400;
    color: var(--text);
    margin: 0;
    border-radius: 8px;
    cursor: pointer;
}
/* One line per book: a long title wrapping to three lines turns a pick-list
   back into a wall of text. The ellipsis lives on the span rather than the
   label, because a flex container can't ellipsise an anonymous text node. */
.series-book-title {
    flex: 1;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.series-book-item + .series-book-item { border-top: 1px solid var(--border-soft); }
.series-book-item:hover { background: var(--border-soft); }
.series-book-item input[type="checkbox"] {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
}

/* Text fields only. A checkbox is an input too, so this was giving every
   checkbox in a card form width:100%, a text field's padding and — the one
   that actually showed — min-height:44px, which beat the 18px height set on
   the box itself. The sign-in "Remember this device" box rendered 18x44,
   sitting in a 50px row. Two places had already patched around it locally
   (.series-book-item, and an inline style on the login page) rather than
   excluding it here. */
form.card input:not([type="checkbox"]):not([type="radio"]),
form.card select, form.card textarea {
    width: 100%;
    padding: 0.75rem 0.9rem;
    border-radius: 9px;
    border: 1px solid var(--border);
    background: var(--bg);
    color: var(--text);
    font-size: 0.95rem;
    font-family: inherit;
    min-height: 44px;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

form.card .submit-row {
    margin-top: 1.75rem;
    display: flex;
    gap: 0.75rem;
}

/* The fixed frosted-glass save bar that used to live here is gone. It was
   scoped to form.card .submit-row.submit-row-sticky, which no template has
   matched since the edit form was restructured — .form-submit replaced it.
   Left in place it was 30 lines of blur, mask and z-index that looked live
   and styled nothing. */

.import-steps {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 16px;
    padding: 1.75rem;
    box-shadow: var(--shadow-md);
}

.import-steps h3 {
    font-size: 1.15rem;
    font-weight: 700;
    letter-spacing: -0.02em;
}

.import-steps table {
    width: 100%;
    border-collapse: collapse;
    margin: 1rem 0;
    font-size: 0.85rem;
}

.import-steps th, .import-steps td {
    border: 1px solid var(--border-soft);
    padding: 0.45rem 0.65rem;
    text-align: left;
    max-width: 220px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.import-steps th { background: var(--border-soft); }

.import-steps select {
    padding: 0.5rem 0.6rem;
    border-radius: 6px;
    border: 1px solid var(--border);
    background: var(--bg);
    color: var(--text);
    font-size: 0.88rem;
    min-height: 40px;
}

.help-text {
    color: var(--muted);
    font-size: 0.85rem;
    line-height: 1.6;
}

.button {
    display: inline-flex;
    align-items: center;
    padding: 0.75rem 1.4rem;
    border-radius: 10px;
    border: none;
    background: linear-gradient(135deg, var(--accent), var(--accent-dark));
    color: white;
    font-weight: 600;
    font-size: 0.92rem;
    min-height: 44px;
    cursor: pointer;
    text-decoration: none;
    box-shadow: var(--shadow-sm);
    transition: transform 0.12s ease, box-shadow 0.12s ease, opacity 0.12s ease;
}

.button:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); }
.button:disabled {
    opacity: 0.45;
    cursor: not-allowed;
    box-shadow: none;
    transform: none;
}

.button.secondary {
    background: var(--card);
    color: var(--text);
    border: 1px solid var(--border);
    box-shadow: none;
}

.button.secondary:hover {
    border-color: var(--accent);
    color: var(--accent);
    box-shadow: none;
    transform: none;
}

/* Mobile header/summary overrides — kept at the end of the file, after every
   base rule they touch, on purpose: a same-specificity rule that appears
   later in source order wins a cascade tie even inside a matching @media
   block, so a copy of this placed earlier got silently overridden by the
   base .summary/.chip rules further down. */
@media (max-width: 640px) {
    .header-align-wrap { padding: 0 16px; }
    header.top {
        flex-direction: column;
        align-items: stretch;
        /* Same 2px reasoning as the base rule above — this mobile override
           had its own separate 0.85rem that was quietly winning the
           cascade on the viewport that actually matters here. Top padding
           padding and margin also match .header/.lib-hero exactly — this
           override has its own copy of both, and it's the one that wins on
           the viewport that actually matters, so the base rule alone being
           correct isn't enough. */
        padding: 10px 2px 0.9rem;
        margin: 2px 0 0;
    }
    header.top h1 { white-space: normal; font-size: 1.35rem; }
    header.top .actions { justify-content: space-between; gap: 0.15rem; }
    header.top .actions .btn-secondary,
    header.top .actions .btn-primary {
        padding-left: 0.65rem;
        padding-right: 0.65rem;
        gap: 0.3em;
        /* Real safety margin, not a hairline fit tuned to one browser's
           font metrics — Safari measures this same text noticeably wider
           than Chrome does, which kept breaking a tightly-tuned fit. */
        font-size: 0.83rem;
    }
    /* Bottom padding clears the fixed .tab-nav bar (see that rule for the
       exact height this adds up to). */
    main { padding: 0.75rem 16px calc(82px + env(safe-area-inset-bottom)); }
    .summary { gap: 0.4rem; flex-wrap: nowrap; }
    .summary .chip { flex: 1; padding: 0.4rem 0.5rem; font-size: 0.78rem; }
}

/* ===== TAB NAV ===== */
/* Global bottom navigation — every page includes _tab_nav.html, so this lives
   here (not trail.css). Edge-to-edge docked bar with a hairline top border,
   replacing the floating rounded pill it used to be, with the primary [+]
   action docked in its middle slot.

   The four per-tab colour tokens (--trail-bg/--chal-bg/--board-bg/--books-bg)
   and their four `.on` gradients are gone: four differently-coloured pill
   cards competed with each other and with the accent, and made the bar the
   loudest thing on every screen. Everything is monochrome slate now, with a
   single accent reserved for whichever tab you're actually on. */
.tab-nav {
    display: flex;
    justify-content: space-around;
    align-items: center;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    /* 58px of content box, down from 64. box-sizing is border-box globally,
       so the env() term below sits inside this figure rather than adding to
       it — the bar is 58px of bar plus however much home indicator the device
       has, not 58+34 of chrome. The old 64 held ~49px of icon and label, so
       15px of it was slack.
       No fallback in the env(): a value like env(..., 16px) would add 16px of
       dead space on every device that has no home indicator at all. */
    height: calc(58px + env(safe-area-inset-bottom));
    padding-bottom: env(safe-area-inset-bottom);
    /* Centres the row on wide screens without breaking the full-bleed border,
       which a max-width on the bar itself would have done. */
    padding-inline: max(0px, calc((100% - var(--content-max)) / 2));
    /* Solid, not translucent. A blurred bar smeared whatever scrolled beneath
       it, which read as muddy against the crisp cards rather than as depth. */
    background: var(--card);
    border-top: 1px solid var(--border);
    z-index: 60;
}
@media (prefers-color-scheme: dark) {
    :root:not([data-theme='light']) .tab-nav { background: #121218;
        border-top-color: rgba(30, 41, 59, 0.9); }
}
:root[data-theme='dark'] .tab-nav { background: #121218;
        border-top-color: rgba(30, 41, 59, 0.9); }


.nav-item {
    flex: 1;
    min-width: 0;
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    padding: 4px;
    border: none;
    background: none;
    text-decoration: none;
    font-family: inherit;
    font-size: 11px;
    font-weight: 500;
    white-space: nowrap;
    min-height: 44px;
    color: var(--nav-idle);
    cursor: pointer;
    transition: color 0.15s ease;
}
.nav-ic { display: flex; }
.nav-ic .icon { width: 22px; height: 22px; }
.nav-lb { line-height: 1.1; }

.nav-item.on { color: var(--nav-active); font-weight: 600; }
/* The active indicator is a 3px dot above the icon, positioned absolutely so
   it can't shift the icon/label pair off-centre the way an in-flow element
   would — the tab would visibly jump as you moved between them. */
.nav-item.on::before {
    content: "";
    position: absolute;
    top: 2px;
    width: 3px;
    height: 3px;
    border-radius: 50%;
    background: currentColor;
    box-shadow: 0 0 6px 1px currentColor;
}
.nav-item:hover { color: var(--nav-active); }

:root {
    --nav-idle: #64748b;
    --nav-active: var(--accent-dark);
}
@media (prefers-color-scheme: dark) {
    :root:not([data-theme='light']) { --nav-idle: #94a3b8;
        --nav-active: #ffffff; }
}
:root[data-theme='dark'] { --nav-idle: #94a3b8;
        --nav-active: #ffffff; }


/* Fades scrolling content out before it reaches the fixed tab-nav,
   instead of cards visibly running into (and being cut off by) that bar —
   same "floats over content, doesn't fight it" fade language as the
   pwa-topbar. Sits above normal content but below the tab-nav (z-index
   60), and pointer-events:none so it never blocks taps on what's
   underneath. One height everywhere now: the per-page floating FABs that
   needed a taller fade are gone, replaced by the docked [+] in the bar. */
/* ===== DOCKED PRIMARY ACTION ===== */
/* Middle slot of .tab-nav, lifted so it breaks the bar's top border — the
   raised silhouette is what marks it as the primary action rather than a
   fifth tab. Replaces the two floating FABs that used to hover over content.

   Monochrome rather than accent-coloured, to match the rest of the bar: white
   on the dark theme, and inverted to near-black on the light one. A white
   circle on the light theme's pale bar would have had almost no edge, and the
   whole point of the shape is that it reads instantly. */
.nav-fab {
    flex: 0 0 auto;
    width: 48px;
    height: 48px;
    border: none;
    border-radius: 50%;
    background: #0f172a;
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    /* Derived, not guessed: the button is centred in the bar first, which
       sits it (58-48)/2 = 5px below the top border, so clearing that border
       by the intended 12px needs 17px of lift. It has to move whenever the
       bar height does. */
    transform: translateY(-17px);
    /* Ring is a spread shadow in the bar's own colour, so the button appears
       punched through the bar rather than sitting on top of it. */
    box-shadow: 0 0 0 4px var(--card), 0 6px 16px rgba(0, 0, 0, 0.35);
    transition: transform 0.12s ease, background 0.15s ease;
}
@media (prefers-color-scheme: dark) {
    :root:not([data-theme='light']) .nav-fab { background: #f1f5f9;
        color: #020617;
        box-shadow: 0 0 0 4px rgba(18, 18, 24, 0.95), 0 6px 16px rgba(0, 0, 0, 0.5); }
}
:root[data-theme='dark'] .nav-fab { background: #f1f5f9;
        color: #020617;
        box-shadow: 0 0 0 4px rgba(18, 18, 24, 0.95), 0 6px 16px rgba(0, 0, 0, 0.5); }

.nav-fab:active { transform: translateY(-17px) scale(0.95); }
.nav-fab-icon { width: 24px; height: 24px; stroke-width: 2.6; transition: transform 0.18s ease; }
/* Rotate to an "x" while the sheet is open, so the button also reads as the
   way to close it. */
.nav-fab.is-open .nav-fab-icon { transform: rotate(45deg); }

/* ===== QUICK ACTION SHEET ===== */
.quick-overlay {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, 0.45);
    z-index: 70;
}
.quick-overlay.hidden, .quick-sheet.hidden { display: none; }
.quick-sheet {
    position: fixed;
    left: 10px;
    right: 10px;
    /* Sits directly above the bar rather than at the screen edge, so the
       button you tapped stays visible and the relationship is obvious.
       Sized off the bar (64px) plus the 12px the [+] rises above it, plus a
       gap — not a round number picked by eye. */
    bottom: calc(92px + env(safe-area-inset-bottom));
    max-width: var(--content-max);
    margin-inline: auto;
    z-index: 80;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 18px;
    box-shadow: var(--shadow-lg);
    padding: 10px;
    display: flex;
    flex-direction: column;
    gap: 8px;
    animation: quick-rise 0.16s ease-out;
}
@keyframes quick-rise {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: translateY(0); }
}
/* Respect a reduced-motion preference — this is decorative. */
@media (prefers-reduced-motion: reduce) {
    .quick-sheet { animation: none; }
    .nav-fab, .nav-fab-icon { transition: none; }
}
.quick-grip {
    width: 36px;
    height: 4px;
    border-radius: 99px;
    background: var(--border);
    margin: 2px auto 4px;
}
.quick-action {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: 14px;
    background: var(--bg);
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
    min-height: 48px;
}
.quick-action:active { transform: scale(0.99); }
.quick-primary {
    background: var(--accent-soft);
    border-color: var(--accent);
}
.quick-ic {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    flex-shrink: 0;
    border-radius: 11px;
    background: var(--card);
    color: var(--accent-dark);
}
.quick-primary .quick-ic { background: var(--accent); color: #fff; }
.quick-ic .icon { width: 20px; height: 20px; }
.quick-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.quick-text strong { font-size: 0.92rem; font-weight: 700; }
.quick-text small { font-size: 0.76rem; color: var(--muted); }

body::after {
    content: "";
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    height: calc(96px + env(safe-area-inset-bottom));
    background: linear-gradient(to bottom, transparent, var(--bg) 60%);
    pointer-events: none;
    z-index: 40;
}

/* Fixed circular FAB, stacked above the global .tab-nav bar — brought
   back after the inline-button alternative turned out to be worse in
   practice: it only sat near the top of the page, so it wasn't reachable
   after scrolling into the list the way the FAB always was. */

/* ===== BOOK EDIT SHEET =====
   Everything that used to be an inline dropdown on the card (format,
   status, who-for) plus the old "···" dropdown's Edit/Get info/Delete now
   lives here — one shared sheet, populated by JS from whichever card was
   tapped, rather than duplicating this markup 600+ times over. */
/* A centred dialog rather than a bottom sheet. Sliding up from the bottom
   edge has no spatial relationship to a card tapped halfway up a list — it
   reads as a system panel arriving rather than as that book opening. This
   scales up in place instead, where the eye already is. */
.book-sheet-overlay {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, 0.5);
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px 16px;
    animation: bs-fade 0.15s ease;
}
.book-sheet-overlay.hidden { display: none; }
.book-sheet {
    background: var(--card);
    border-radius: 18px;
    padding: 20px;
    width: 100%;
    max-width: 420px;
    /* Bounded by the padded overlay, so the dialog can never reach the screen
       edges however long the blurb is. */
    max-height: 100%;
    overflow-y: auto;
    box-shadow: var(--shadow-lg);
    animation: bs-pop 0.18s cubic-bezier(0.34, 1.4, 0.64, 1);
}
@keyframes bs-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes bs-pop { from { opacity: 0; transform: scale(0.94); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
    .book-sheet-overlay, .book-sheet { animation: none; }
}
/* The grab handle went with the sheet — there's no drag-to-dismiss on a
   centred dialog, and a handle that doesn't do anything is a false affordance. */
.book-sheet-handle { display: none; }
.book-sheet-header { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; }
.book-sheet-cover {
    width: 44px;
    height: 62px;
    border-radius: 5px;
    background: var(--border-soft);
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    color: var(--muted);
}
.book-sheet-cover img { width: 100%; height: 100%; object-fit: cover; }
.book-sheet-header-text { flex: 1; min-width: 0; }
/* Two lines maximum. Amazon-import titles carry a marketing subtitle
   ("...: The Sunday Times Number One Bestseller"), which ran to three or four
   lines here and pushed the status control down the sheet. */
.book-sheet-title {
    font-weight: 700;
    font-size: 1rem;
    margin: 0;
    line-height: 1.3;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.book-sheet-author { font-size: 0.85rem; color: var(--muted); margin: 2px 0 0; }
.book-sheet-close {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    border: none;
    background: var(--border-soft);
    color: var(--muted);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    flex-shrink: 0;
}
.book-sheet-close:hover { background: var(--border); }
.book-sheet-close .icon { width: 22px; height: 22px; }
.book-sheet-row2 { display: flex; gap: 10px; }
.book-sheet-field { margin-bottom: 14px; flex: 1; min-width: 0; }
.book-sheet-field label {
    display: block;
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
    margin-bottom: 4px;
}
.book-sheet-field select {
    width: 100%;
    background: var(--border-soft);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 0.6rem 0.7rem;
    font-size: 0.88rem;
    color: var(--text);
    font-family: inherit;
    cursor: pointer;
}
.book-sheet-readonly {
    background: var(--border-soft);
    border-radius: 8px;
    padding: 0.6rem 0.7rem;
    font-size: 0.85rem;
    color: var(--text);
    line-height: 1.5;
}
.book-sheet-readonly-empty { color: var(--muted); font-style: italic; }

/* ===== PULL TO REFRESH ===== */
/* Sits above the pwa-topbar (z-index 200) so it's visible even in
   standalone mode, where this gesture is reimplemented at the page level
   in the first place because the OS-level one isn't available there. */
.pull-refresh {
    position: fixed;
    top: calc(env(safe-area-inset-top) + 10px);
    left: 50%;
    --pull: 0px;
    transform: translate(-50%, calc(-46px + var(--pull)));
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: var(--card);
    border: 1px solid var(--border);
    box-shadow: var(--shadow-md);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 220;
    opacity: 0;
    pointer-events: none;
}
.pull-refresh.active { opacity: 1; }
/* Only while settling — never while the finger is down, where a transition
   would make the indicator lag behind the touch. */
.pull-refresh.settling { transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.2s ease; }
.pull-refresh.refreshing { opacity: 1; }
.pull-refresh-spin {
    width: 16px;
    height: 16px;
    border: 2px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    transition: transform 0.15s ease;
}
.pull-refresh.ready .pull-refresh-spin { transform: rotate(180deg); border-top-color: var(--accent-dark); }
.pull-refresh.refreshing .pull-refresh-spin { animation: pull-refresh-spin 0.7s linear infinite; }
@keyframes pull-refresh-spin { to { transform: rotate(360deg); } }

/* ===== LIBRARY SEARCH HEADER ===== */
/* One filled row carrying search, clear and the filter entry point, replacing
   the eight-control form matrix this page used to open with. Everything that
   isn't text search now lives behind the sliders button, because those are
   set-and-forget choices and search is the one you touch constantly. */
.lib-filter-form { display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px; }

.search-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    background: var(--border-soft);
    border-radius: 16px;
    padding: 4px 6px 4px 14px;
}
.search-bar-icon { width: 18px; height: 18px; color: var(--muted); flex-shrink: 0; }
.search-bar input[type="search"] {
    flex: 1;
    min-width: 0;
    height: 42px;
    border: none;
    background: none;
    font: inherit;
    font-size: 0.95rem;
    color: var(--text);
    /* Safari draws its own clear affordance on type=search, which would sit
       next to ours. */
    -webkit-appearance: none;
    appearance: none;
}
.search-bar input[type="search"]::-webkit-search-cancel-button { display: none; }
.search-bar input:focus { outline: none; }

.search-clear, .search-filter-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    border: none;
    background: none;
    color: var(--muted);
    cursor: pointer;
    border-radius: 10px;
}
.search-clear { width: 30px; height: 30px; }
.search-clear .icon { width: 15px; height: 15px; }
.search-clear.hidden { display: none; }
.search-filter-btn {
    position: relative;
    width: 38px;
    height: 38px;
    background: var(--card);
    color: var(--text);
    box-shadow: var(--shadow-sm);
}
.search-filter-btn .icon { width: 19px; height: 19px; }
.select-mode-btn[aria-pressed="true"] { background: var(--accent); color: #fff; }
.search-filter-btn .btn-label { display: none; }
/* Bulk-select has only ever been a desktop thing (see the comment on the
   checkbox column further up) — the button itself needs the same gate the
   checkboxes already have, not just the checkboxes. Missing this was a real
   bug: the button showed on mobile looking tappable but did nothing visible
   there, since the checkbox column it turns on stays hidden below 641px
   regardless. */
.select-mode-btn { display: none; }
@media (min-width: 641px) {
    .select-mode-btn { display: flex; }
    /* Icon-only made sense when the search bar was tight; at this width
       there's room to spare, so words instead of a guessing game. */
    .search-filter-btn {
        width: auto;
        padding: 0 14px;
        gap: 6px;
    }
    .search-filter-btn .btn-label {
        display: inline;
        font-size: 0.85rem;
        font-weight: 600;
    }
}
.search-filter-badge {
    position: absolute;
    top: -3px;
    right: -3px;
    min-width: 17px;
    height: 17px;
    padding: 0 4px;
    border-radius: 999px;
    background: var(--accent);
    color: #fff;
    font-size: 0.62rem;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 2px solid var(--bg);
}

/* Horizontally scrollable so more chips can be added without ever wrapping
   into a second row and pushing the book list down. */
.quick-chips {
    display: flex;
    gap: 6px;
    /* overflow-x: auto makes the other axis clip too, so a chip's expanded hit
       area would be sliced off at this row's 25px height. The padding gives
       the scroller room to hold it and the matching negative margin puts the
       row back exactly where it was — nothing moves. */
    padding: 9px 0;
    margin: -9px 0;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}
.quick-chips::-webkit-scrollbar { display: none; }
/* Drawn at 25px, tapped at 43x47. 9px vertical fits the 10px of clearance to
   the search bar above; 3px a side exactly meets the neighbouring chip across
   the 6px gap without crossing into it, so no tap is ambiguous. */
.qchip { position: relative; }
.qchip::before { content: ""; position: absolute; inset: -9px -3px; }
.qchip {
    flex-shrink: 0;
    padding: 5px 13px;
    border-radius: 999px;
    background: var(--border-soft);
    color: var(--sub);
    font-size: 0.76rem;
    font-weight: 600;
    text-decoration: none;
    white-space: nowrap;
}
.qchip.on { background: var(--accent); color: #fff; }
.qchip-sort { margin-left: auto; background: var(--card); box-shadow: var(--shadow-sm); }

/* Selectors carry the full table path because `table.books td:first-child`
   sets display:none for the row-select checkbox column at (0,1,2) — and this
   row's only cell IS a first child, so a shorter selector lost and the
   "no matches" message never appeared. */
table.books tbody tr.search-empty-row { display: block; }
table.books tbody tr.search-empty-row td {
    display: block;
    padding: 1.6rem 0;
    text-align: center;
    color: var(--muted);
    font-size: 0.9rem;
}
table.books tbody tr.search-empty-row[hidden] { display: none; }

/* ===== FILTER & SORT SHEET ===== */
.sheet-overlay {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, 0.45);
    z-index: 90;
}
.sheet-overlay.hidden, .filter-sheet.hidden { display: none; }
.filter-sheet {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 100;
    max-width: 560px;
    margin-inline: auto;
    display: flex;
    flex-direction: column;
    /* Caps at 85vh so the Apply button is always reachable without the sheet
       growing taller than the screen on a small phone. */
    max-height: 85vh;
    background: var(--card);
    border-radius: 22px 22px 0 0;
    box-shadow: var(--shadow-lg);
    /* Was pinned to bottom:0 with no allowance, so its last row sat in the
       home-indicator band — the only bottom-anchored surface in the app
       still doing that. */
    padding-bottom: env(safe-area-inset-bottom);
    animation: sheet-rise 0.2s ease-out;
}
@keyframes sheet-rise { from { transform: translateY(14px); opacity: 0; } to { transform: none; opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .filter-sheet { animation: none; } }
.sheet-grip { width: 38px; height: 4px; border-radius: 99px; background: var(--border); margin: 10px auto 2px; }
.sheet-head { display: flex; align-items: center; justify-content: space-between; padding: 6px 18px 10px; }
.sheet-head h2 { font-size: 1.02rem; font-weight: 700; }
.sheet-close {
    width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
    border: none; border-radius: 50%; background: var(--border-soft); color: var(--muted); cursor: pointer;
}
.sheet-close .icon { width: 15px; height: 15px; }
.sheet-body { overflow-y: auto; padding: 0 18px 6px; }
.sheet-group { margin-bottom: 18px; }
.sheet-label {
    display: block;
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
    margin-bottom: 9px;
}
.pill-row { display: flex; flex-wrap: wrap; gap: 7px; }
.sheet-dir { margin-top: 7px; }
/* The radio/checkbox stays in the DOM and does the work — the pill is just
   its label. Keeps the whole sheet a plain form that submits without JS. */
.pill input { position: absolute; opacity: 0; pointer-events: none; }
.pill span {
    display: inline-block;
    padding: 7px 14px;
    border-radius: 999px;
    background: var(--border-soft);
    color: var(--sub);
    font-size: 0.82rem;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.12s ease, color 0.12s ease;
}
.pill span em { font-style: normal; opacity: 0.6; font-weight: 600; margin-left: 2px; }
.pill input:checked + span { background: var(--accent); color: #fff; }
.pill input:focus-visible + span { box-shadow: 0 0 0 3px var(--accent-soft); }
.sheet-select {
    width: 100%;
    height: 44px;
    border-radius: 10px;
    border: 1px solid var(--border);
    background: var(--card);
    color: var(--text);
    font: inherit;
    font-size: 0.92rem;
    padding: 0 2.25rem 0 0.9rem;
    appearance: none;
    -webkit-appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%2364748b' stroke-width='1.6'%3E%3Cpath d='M5 7l5 5 5-5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.85rem center;
    background-size: 14px;
}
.sheet-actions {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 18px calc(14px + env(safe-area-inset-bottom));
    border-top: 1px solid var(--border-soft);
}
.sheet-reset { color: var(--muted); font-size: 0.87rem; font-weight: 600; text-decoration: none; }
.sheet-apply {
    flex: 1;
    height: 46px;
    border: none;
    border-radius: 12px;
    background: var(--accent);
    color: #fff;
    font: inherit;
    font-size: 0.95rem;
    font-weight: 700;
    cursor: pointer;
}
.sheet-apply:hover { background: var(--accent-dark); }

/* ===== OWNER BADGE ===== */
/* Sits on the cover's corner. Colour comes from a data-owner attribute rather
   than a hardcoded per-name list, so a new family member gets a stable colour
   without a code change. */
.for-dot {
    position: absolute;
    bottom: -4px;
    right: -4px;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--accent);
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 0 0 2px var(--card);
}
/* One colour per person, shared by the card's dot and the detail dialog's.
   Hardcoded rather than derived from the name — an unlisted person falls back
   to the accent colour, so adding a fifth profile means adding a line here. */
.for-dot[data-owner="Catrine"], .bs-chip-dot[data-owner="Catrine"] { background: #7c5cff; }
.for-dot[data-owner="Paul"],    .bs-chip-dot[data-owner="Paul"]    { background: #0ea5e9; }
.for-dot[data-owner="Ted"],     .bs-chip-dot[data-owner="Ted"]     { background: #10b981; }
.for-dot[data-owner="Anna"],    .bs-chip-dot[data-owner="Anna"]    { background: #f43f5e; }

/* ===== QUICK ACTIONS ===== */
.row-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.quick-toggle {
    width: 30px;
    height: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: none;
    border-radius: 8px;
    cursor: pointer;
    color: var(--border);
    transition: transform 0.12s ease, color 0.12s ease;
}
.quick-toggle .icon { width: 17px; height: 17px; }
/* The kebab beside it has carried an expanded hit area for a while; this one
   never did, so two controls 6px apart offered 30px and 44px targets. Both are
   44px now — still drawn at 30px, so nothing moves visually. */
.quick-toggle { position: relative; }
.quick-toggle::before { content: ""; position: absolute; inset: -7px -3px; }
.quick-toggle:active { transform: scale(0.85); }
.quick-toggle:hover { color: #fb7185; }
.fav-toggle.on { color: #f43f5e; }
/* Filled rather than outlined once active, so the state reads at a glance
   and not only by hue. */
.quick-toggle.on .icon { fill: currentColor; }
/* ===== SERIES STACKS ===== */
table.books tbody tr.series-stack {
    display: block;
    margin-bottom: 12px;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 16px;
    box-shadow: var(--shadow-sm);
    cursor: pointer;
}
table.books tbody tr.series-stack td { display: block; padding: 14px; }
/* Same reason as tr.book-row[hidden] above: this rule's own display:block is
   an author style, so it beats the browser's [hidden] no matter the
   specificity. Without this, hiding a stack from JS did nothing at all — the
   series card stayed on screen through every search. */
table.books tbody tr.series-stack[hidden] { display: none; }
.stack-head { display: flex; align-items: center; gap: 12px; }
/* Overlapping covers, offset with negative margins, to read as a stack. */
.stack-covers { display: flex; flex-shrink: 0; }
.stack-covers img, .stack-covers .stack-blank {
    width: 40px;
    height: 58px;
    border-radius: 5px;
    object-fit: cover;
    box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.08), var(--shadow-sm);
    background: var(--border-soft);
}
.stack-covers > * + * { margin-left: -22px; }
.stack-info { flex: 1; min-width: 0; }
.stack-title { font-weight: 700; font-size: 0.95rem; display: flex; align-items: center; gap: 6px; }
.stack-title .icon { width: 15px; height: 15px; flex-shrink: 0; color: var(--accent-dark); }
.stack-sub { font-size: 0.78rem; color: var(--muted); margin-top: 2px; }
.stack-bar { height: 6px; border-radius: 999px; background: var(--border-soft); margin-top: 8px; overflow: hidden; }
.stack-bar-fill { height: 100%; background: var(--accent); border-radius: 999px; }
.stack-chevron { flex-shrink: 0; color: var(--muted); transition: transform 0.18s ease; }
.stack-chevron .icon { width: 18px; height: 18px; }
tr.series-stack.open .stack-chevron { transform: rotate(180deg); }
.stack-trophy { color: #d97706; }
.stack-trophy .icon { width: 15px; height: 15px; }

/* Members are indented while grouped, so the stack reads as their parent. */
table.books tbody tr.book-row.in-series { margin-left: 14px; }

/* ===== UNOWNED VOLUME PLACEHOLDER ===== */
table.books tbody tr.ghost-vol {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 0 0 12px 14px;
    padding: 12px 14px;
    border: 2px dashed var(--border);
    border-radius: 16px;
    background: color-mix(in srgb, var(--border-soft) 55%, transparent);
}
table.books tbody tr.ghost-vol td { display: contents; }
/* Likewise — and this one was never right: ghost volumes are created with
   hidden set, so they showed the moment a series was grouped rather than
   waiting for the stack to be opened. */
table.books tbody tr.ghost-vol[hidden] { display: none; }
.ghost-cover {
    width: 40px;
    height: 58px;
    border-radius: 5px;
    border: 2px dashed var(--border);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--muted);
    font-size: 0.72rem;
    font-weight: 700;
    flex-shrink: 0;
}
.ghost-info { flex: 1; min-width: 0; }
/* Both are <span> inside a td using display:contents, so they need to be told
   to stack — as inline elements the title and series name ran together mid
   sentence. */
.ghost-title { display: block; font-size: 0.85rem; font-weight: 600; color: var(--muted); }
.ghost-sub { display: block; font-size: 0.74rem; color: var(--muted); opacity: 0.85; margin-top: 1px; }
.ghost-add {
    flex-shrink: 0;
    padding: 7px 12px;
    border: none;
    border-radius: 9px;
    background: var(--accent-soft);
    color: var(--accent-dark);
    font: inherit;
    font-size: 0.78rem;
    font-weight: 700;
    cursor: pointer;
}
.ghost-add:active { transform: scale(0.96); }

/* A 2-column layout (tried two ways — CSS Grid with a spanning open series,
   then independent flexbox columns) always traded one problem for another:
   grid kept cards row-aligned but reflowed the sibling column whenever a
   series opened; independent columns fixed that but let cards drift out of
   row-sync as soon as one card was taller than its row-mate. Simplest and
   least surprising is not to solve that tension at all — same one-column
   list as mobile, just capped and centered like a form, so the far right of
   a wide screen isn't stretching cards edge to edge. */
@media (min-width: 900px) {
    table.books {
        max-width: 640px;
        margin: 0 auto;
    }
}

/* ===== GROUP BY SERIES SWITCH ===== */
.sheet-switch { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.sheet-switch-label { font-size: 0.9rem; font-weight: 600; }
.sheet-switch-hint { font-size: 0.76rem; color: var(--muted); margin-top: 2px; }
.switch { position: relative; width: 46px; height: 27px; flex-shrink: 0; }
.switch input { position: absolute; opacity: 0; pointer-events: none; }
.switch-track {
    display: block;
    width: 46px;
    height: 27px;
    border-radius: 999px;
    background: var(--border);
    transition: background 0.15s ease;
    cursor: pointer;
}
.switch-track::after {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 21px;
    height: 21px;
    border-radius: 50%;
    background: #fff;
    box-shadow: var(--shadow-sm);
    transition: transform 0.15s ease;
}
.switch input:checked + .switch-track { background: var(--accent); }
.switch input:checked + .switch-track::after { transform: translateX(19px); }

/* Gold star beside the title for a book flagged Up Next. Only rendered when
   the flag is set — the previous approach put an outline control on every
   card whether or not it meant anything. */
.upnext-star { display: inline-flex; vertical-align: -2px; margin-right: 4px; color: #f0b429; }
.upnext-star .icon { width: 14px; height: 14px; fill: currentColor; }

/* ===== STATUS POPOVER ===== */
.status-pop {
    position: fixed;
    z-index: 120;
    min-width: 148px;
    padding: 6px;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 12px;
    box-shadow: var(--shadow-lg);
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.status-pop button {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 8px 10px;
    border: none;
    border-radius: 8px;
    background: none;
    color: var(--text);
    font: inherit;
    font-size: 0.86rem;
    text-align: left;
    cursor: pointer;
}
.status-pop button:hover { background: var(--border-soft); }
.status-pop button.current { font-weight: 700; }
.status-pop .dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
.status-pop .dot.unread  { background: #94a3b8; }
.status-pop .dot.reading { background: #f59e0b; }
.status-pop .dot.read    { background: #10b981; }
.status-pop .dot.sample  { background: #0ea5e9; }

/* ===== CARD METADATA ===== */
/* Purchase context on its own line, deliberately quieter than the author —
   it's reference detail, not something you scan a shelf by. */
.book-acq {
    font-size: 0.72rem;
    color: var(--muted);
    opacity: 0.85;
    margin-top: 3px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Neutral chip carrying the format as an icon plus a word. It replaces a
   coloured square whose fill encoded the retailer — that information moved to
   the acquisition line above, where it reads as purchase context rather than
   as a colour you had to learn. */
.fmt-pill {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    /* This and .status-badge sit side by side on the same row and rendered
       18px against 19px. Their font sizes differ, so padding alone can't
       equalise them — a shared min-height can, and does it exactly. */
    padding: 2px 8px;
    min-height: 20px;
    border-radius: 6px;
    background: var(--border-soft);
    color: var(--sub);
    font-size: 0.7rem;
    font-weight: 600;
    white-space: nowrap;
    flex-shrink: 0;
}
.fmt-pill .icon { width: 13px; height: 13px; }
/* Its own pill beside the format one, rather than a compound "Kindle sample"
   label: the format is a fact about the book and shouldn't change wording
   because of it. A dashed edge carries the same "not the whole thing" idea as
   the unowned-volume placeholders. Padding is 1px tighter than .fmt-pill so
   the border doesn't make it taller than the pill next to it. */
.sample-pill {
    display: inline-flex;
    align-items: center;
    padding: 1px 7px;
    border-radius: 6px;
    background: color-mix(in srgb, var(--sample) 15%, var(--card));
    border: 1px dashed color-mix(in srgb, var(--sample) 55%, var(--card));
    color: color-mix(in srgb, var(--sample) 70%, #1e1b34);
    font-size: 0.7rem;
    font-weight: 600;
    white-space: nowrap;
    flex-shrink: 0;
}
@media (prefers-color-scheme: dark) {
    :root:not([data-theme='light']) .sample-pill { background: color-mix(in srgb, var(--sample) 22%, #0f172a);
        color: color-mix(in srgb, var(--sample) 65%, #ffffff); }
}
:root[data-theme='dark'] .sample-pill { background: color-mix(in srgb, var(--sample) 22%, #0f172a);
        color: color-mix(in srgb, var(--sample) 65%, #ffffff); }

.status-badge { border-radius: 999px; padding: 3px 10px; }

/* ===== PROFILE ROLES ===== */
.profile-row-btn {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 12px;
    border: 1px solid var(--border);
    border-radius: 14px;
    background: var(--card);
    font: inherit;
    text-align: left;
    cursor: pointer;
    margin-bottom: 8px;
}
/* Same 36px block as .sec-row-ic in the preferences below. The emoji was a
   bare 16px glyph, so the two sections' text columns started 20px apart and
   the page read as two unrelated lists. */
.profile-row-btn .avatar,
.grp-item .avatar {
    flex-shrink: 0;
    width: 36px;
    height: 36px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 1.35rem;
    line-height: 1;
}
.profile-row-btn .info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; align-items: flex-start; }
.profile-row-btn .name { font-size: 1rem; font-weight: 600; color: var(--text); }
/* Text and, for the Owner, a lock — not a filled pill. The pill had to
   restate a background for every theme, and the Member one was the thing that
   read worst in dark mode. Colour alone separates the roles now: the accent
   for whoever can change things, muted for whoever can't. */
.role-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    background: none;
    padding: 0;
    border-radius: 0;
    font-size: 0.74rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--muted);
    white-space: nowrap;
}
.role-chip .icon { width: 12px; height: 12px; }
.role-chip.primary, .role-chip.admin { color: var(--accent-dark); }
.role-chip.member { color: var(--muted); }
/* Tokens rather than literals. This was #1e293b on #cbd5e1 in dark mode —
   cold slate, on a palette whose surfaces are warm (--card #29242f), so the
   pill read as a blue island sitting on a purple-grey card. Both tokens flip
   with the theme, so the media query that used to patch it is gone. */
.profile-chevron { color: var(--muted); display: flex; transition: transform 0.18s ease; }
.profile-chevron .icon { width: 18px; height: 18px; }
.profile-row-btn.open .profile-chevron { transform: rotate(180deg); }

/* ===== PERMISSIONS SHEET ===== */
.perm-sheet {
    margin: -2px 0 12px;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: 14px;
    background: var(--bg);
}
.perm-sheet.hidden { display: none; }
.perm-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.perm-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    padding: 10px 0;
    border-top: 1px solid var(--border-soft);
    cursor: pointer;
}
.perm-label { display: flex; flex-direction: column; gap: 2px; font-size: 0.88rem; font-weight: 600; color: var(--text); }
.perm-label small { font-size: 0.74rem; font-weight: 400; color: var(--muted); }
.perm-member-only.hidden { display: none; }
.perm-actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 12px; }
.perm-delete { border: none; background: none; color: #e11d48; font: inherit; font-size: 0.84rem; font-weight: 600; cursor: pointer; padding: 0; }
.perm-locked { display: inline-flex; align-items: center; gap: 5px; font-size: 0.78rem; color: var(--muted); }
.perm-locked .icon { width: 13px; height: 13px; }
.perm-save {
    padding: 9px 18px;
    border: none;
    border-radius: 10px;
    background: var(--accent);
    color: #fff;
    font: inherit;
    font-weight: 700;
    cursor: pointer;
}

/* ===== ADD BOOK ===== */
body.add-body { background: var(--bg); }
.add-shell {
    max-width: 32rem;
    margin: 0 auto;
    padding: calc(env(safe-area-inset-top) + 14px) 16px calc(24px + env(safe-area-inset-bottom));
}
.add-top { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
.add-top h1 {
    flex: 1; text-align: center;
    font-family: 'Baloo 2', sans-serif;   /* see .page-head h1 */
    font-size: 1.1rem; font-weight: 800; letter-spacing: -0.02em;
}
.add-meta-status { min-width: 36px; text-align: right; font-size: 0.7rem; font-weight: 600; color: var(--muted); }
.add-meta-status.ok { color: #047857; }
.add-meta-status.warn { color: #b45309; }

.scan-cta {
    display: flex; align-items: center; justify-content: center; gap: 8px;
    width: 100%; padding: 13px 16px; margin-bottom: 14px;
    border: 1px solid #ddd6fe; border-radius: 16px;
    background: #f5f3ff; color: #6d28d9;
    font: inherit; font-size: 0.88rem; font-weight: 700; cursor: pointer;
    transition: background 0.15s ease, transform 0.1s ease;
}
.scan-cta:hover { background: #ede9fe; }
.scan-cta:active { transform: scale(0.99); }
.scan-cta-icon { width: 17px; height: 17px; }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme='light']) .scan-cta { background: rgba(76, 29, 149, 0.35); border-color: #5b21b6; color: #c4b5fd; }
}
:root[data-theme='dark'] .scan-cta { background: rgba(76, 29, 149, 0.35); border-color: #5b21b6; color: #c4b5fd; }


.lookup { position: relative; margin-bottom: 18px; }
.lookup-box { display: flex; align-items: center; gap: 8px; background: var(--border-soft); border-radius: 14px; padding: 0 12px; }
.lookup-icon { width: 17px; height: 17px; color: var(--muted); flex-shrink: 0; }
.lookup-box input { flex: 1; min-width: 0; height: 46px; border: none; background: none; font: inherit; font-size: 0.92rem; color: var(--text); }
.lookup-box input:focus { outline: none; }
.lookup-spin { width: 14px; height: 14px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.lookup-results {
    position: absolute; left: 0; right: 0; top: calc(100% + 6px); z-index: 20;
    background: var(--card); border: 1px solid var(--border); border-radius: 14px;
    box-shadow: var(--shadow-lg); overflow: hidden; max-height: 320px; overflow-y: auto;
}
.lookup-results.hidden { display: none; }
.lookup-hit { display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px 12px; border: none; background: none; font: inherit; text-align: left; cursor: pointer; }
.lookup-hit:hover { background: var(--border-soft); }
.lookup-hit img, .lookup-blank { width: 34px; height: 48px; object-fit: cover; border-radius: 4px; background: var(--border-soft); flex-shrink: 0; }
.lookup-hit-text { display: flex; flex-direction: column; min-width: 0; }
.lookup-hit-text strong { font-size: 0.87rem; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lookup-hit-text small { font-size: 0.75rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lookup-empty { padding: 14px; font-size: 0.84rem; color: var(--muted); text-align: center; }

.route-teaser { background: var(--border-soft); border: 1px solid var(--border); border-radius: 16px; padding: 14px; margin-bottom: 18px; }
.route-teaser.hidden { display: none; }
.route-dist { font-size: 0.92rem; color: var(--text); }
.route-dist strong { color: var(--accent-dark); }
.route-impact { font-size: 0.78rem; color: var(--muted); margin-top: 6px; }
.route-arrow { color: var(--accent); font-weight: 700; }

.fld-label { display: block; font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin: 14px 0 5px; }
.fld {
    width: 100%; min-width: 0; padding: 12px 14px; border-radius: 12px;
    border: 1px solid var(--border); background: var(--border-soft);
    color: var(--text); font: inherit; font-size: 0.9rem;
    transition: border-color 0.12s ease, background 0.12s ease, box-shadow 0.12s ease;
}
.fld:focus { outline: none; border-color: var(--accent); background: var(--card); box-shadow: 0 0 0 3px var(--accent-soft); }

.fmt-seg { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; padding: 5px; background: var(--border-soft); border-radius: 14px; }
.fmt-seg > label, .fmt-seg span { min-width: 0; }
.fmt-seg input { position: absolute; opacity: 0; pointer-events: none; }
.fmt-seg span { display: flex; align-items: center; justify-content: center; gap: 5px; padding: 9px 4px; border-radius: 10px; font-size: 0.76rem; font-weight: 600; color: var(--muted); cursor: pointer; }
.fmt-seg .icon { width: 14px; height: 14px; }
.fmt-seg input:checked + span { background: var(--card); color: var(--accent-dark); box-shadow: var(--shadow-sm); }

.audio-progress { margin-top: 12px; }
.audio-progress.hidden { display: none; }
.audio-progress input[type="range"] { width: 100%; accent-color: var(--accent); }
.audio-note { font-size: 0.76rem; color: var(--muted); margin-top: 4px; }

/* min-width:0 on the children matters more than it looks. Grid items default
   to min-width:auto — they will not shrink below their content — and a native
   date input renders far wider on iOS ("15 Aug 2026" plus a stepper) than in
   a desktop browser ("dd/mm/yyyy"). Without this the row pushes the whole
   page sideways on a phone while measuring perfectly on a desktop. */
.acq-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.acq-grid > * { min-width: 0; }
.acq-grid input { min-width: 0; }
/* Owner picker: a select carrying the person's colour dot, matching
   .bs-owner in the book detail dialog. Padding, radius, border and
   background mirror .fld exactly so it sits flush with Title, Author and
   Source rather than reading as a different kind of control.
   This replaced a row of tappable avatar circles — the only control of its
   kind in the app, and the one that needed a horizontal scroll once there
   were more than five profiles. */
.owner-field {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-width: 0;
    padding: 12px 14px;
    border-radius: 12px;
    border: 1px solid var(--border);
    background: var(--border-soft);
    cursor: pointer;
}
.owner-field:focus-within { border-color: var(--accent); background: var(--card); box-shadow: 0 0 0 3px var(--accent-soft); }
.owner-field select {
    flex: 1;
    min-width: 0;
    border: none;
    background: none;
    font: inherit;
    font-size: 0.9rem;
    color: var(--text);
    padding: 0;
    cursor: pointer;
}
.owner-field select:focus { outline: none; }
.owner-field-dot {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 20px so the field lands at 46px, the same height as the Source select
       directly below it — at 26px it stood 6px taller than every other field. */
    width: 20px;
    height: 20px;
    flex-shrink: 0;
    border-radius: 50%;
    background: var(--accent);
    color: #fff;
    font-size: 0.62rem;
    font-weight: 700;
}
.owner-field-dot.is-unassigned { background: var(--border); color: var(--muted); }
.owner-field-dot[data-owner="Catrine"] { background: #7c5cff; }
.owner-field-dot[data-owner="Paul"]    { background: #0ea5e9; }
.owner-field-dot[data-owner="Ted"]     { background: #10b981; }
.owner-field-dot[data-owner="Anna"]    { background: #f43f5e; }

/* Held clear of the very bottom edge rather than flush to it. A button
   sitting in the home-indicator band gets the platform's own scroll-edge
   treatment painted over it — the same violet reads pale with washed-out
   text there, while an identical button mid-page renders solid. Nothing in
   this stylesheet causes it (no mask, no filter, nothing overlapping), so
   the fix is to stay out of that band. */
.add-submit-wrap {
    position: sticky;
    bottom: calc(env(safe-area-inset-bottom) + 20px);
    padding: 16px 0 0;
    background: linear-gradient(to bottom, transparent, var(--bg) 30%);
    margin-top: 20px;
    margin-bottom: calc(env(safe-area-inset-bottom) + 20px);
}
.add-submit {
    width: 100%; padding: 14px; border: none; border-radius: 16px;
    background: var(--accent); color: #fff; font: inherit; font-size: 0.95rem; font-weight: 700;
    box-shadow: 0 6px 18px rgba(124, 92, 255, 0.28); cursor: pointer;
    transition: background 0.15s ease, transform 0.1s ease;
}
.add-submit:hover { background: var(--accent-dark); }
.add-submit:active { transform: scale(0.99); }

/* ===== ADMIN SECURITY ROW ===== */
.sec-row {
    display: flex;
    align-items: flex-start;
    /* Wraps so the action drops to its own full-width row on a narrow screen
       instead of stealing width from the text beside it. */
    flex-wrap: wrap;
    gap: 12px;
    padding: 14px;
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 14px;
}
.sec-row-titleline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.sec-row .sec-row-btn { flex: 1 0 100%; padding: 10px; text-align: center; }
.sec-row-ic { display: flex; width: 36px; height: 36px; align-items: center; justify-content: center; border-radius: 10px; background: var(--accent-soft); color: var(--accent-dark); flex-shrink: 0; }
.sec-row-ic .icon { width: 17px; height: 17px; }
.sec-row-text { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.sec-row-text strong { font-size: 0.92rem; font-weight: 600; }
.sec-row-text small { font-size: 0.75rem; line-height: 1.4; color: var(--muted); }
.sec-row-badge { font-size: 0.66rem; font-weight: 700; padding: 3px 8px; border-radius: 999px; background: var(--ok-bg); color: var(--ok-fg); flex-shrink: 0; }
.sec-row-btn { flex-shrink: 0; padding: 6px 12px; border: none; border-radius: 12px; background: var(--border-soft); color: var(--text); font: inherit; font-size: 0.8rem; font-weight: 600; cursor: pointer; }
.sec-row-btn.primary { background: var(--accent-soft); color: var(--accent-dark); font-weight: 700; }
/* ===== PIN SHEET ===== */
.pin-overlay { position: fixed; inset: 0; background: rgba(15,23,42,0.5); z-index: 110; }
.pin-overlay.hidden, .pin-sheet.hidden { display: none; }
.pin-sheet {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 120;
    max-width: 460px; margin-inline: auto;
    background: var(--card); border-radius: 22px 22px 0 0; box-shadow: var(--shadow-lg);
    padding: 0 18px calc(20px + env(safe-area-inset-bottom));
}
.pin-sheet-head { display: flex; align-items: center; justify-content: space-between; padding: 4px 0 6px; }
.pin-sheet-head h2 { font-size: 1.05rem; font-weight: 700; }
.pin-step { font-size: 0.85rem; color: var(--muted); margin-bottom: 16px; }
/* One PIN field, shared by the Settings sheet and the lock screen. A run of
   single-character cells lived here and had to go: it was fixed at four while
   the server accepts four to six, so a longer PIN could not be typed at all.
   Styled to still read as a PIN — centred, spaced, oversized — without
   committing to a length. */
.pin-input {
    width: 100%;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--bg);
    color: var(--text);
    font-family: inherit;
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: 0.42em;
    text-indent: 0.42em;   /* re-centres text that letter-spacing pushes left */
    text-align: center;
    min-height: 52px;
}
.pin-input:focus {
    outline: none;
    border-color: var(--accent);
    background: var(--card);
    box-shadow: 0 0 0 3px var(--accent-soft);
}
.pin-msg { min-height: 20px; margin-top: 12px; text-align: center; font-size: 0.82rem; color: #e11d48; }
.pin-forgot-box { margin-top: 6px; }
.pin-forgot-box.hidden { display: none; }
.pin-actions { display: flex; align-items: center; justify-content: space-between; margin-top: 14px; }
.pin-forgot, .pin-disable { border: none; background: none; font: inherit; font-size: 0.82rem; font-weight: 600; cursor: pointer; padding: 4px 0; }
.pin-forgot { color: var(--muted); }
.pin-disable { color: #e11d48; }

/* ===== SETTINGS PAGE HEADER ===== */
/* Plain header rather than a card. The old hero spent a full card on a title
   and two links the tab bar and topbar already offer. */
.page-head { display: flex; align-items: center; gap: 10px; padding: 12px 0 4px; }
/* margin: 0 because this h1 was otherwise picking up the browser's default
   0.67em top margin — 13.4px, which inflated .page-head from 52px to 67px on
   every page using it and pushed the title and back button down. That's what
   made the library's header look out of line: it was the only correct one. */
/* Baloo, as on the Library and Trail headings. This is the same role — the
   page's title — and it was the only one still set in Inter, on Stats, Wish
   List, Switch Profile, Settings and Edit Book. Weight 800 because that is
   the only Baloo weight the pages load. */
.page-head h1 {
    flex: 1; margin: 0;
    font-family: 'Baloo 2', sans-serif;
    font-size: 1.25rem; font-weight: 800; letter-spacing: -0.02em;
}
.page-back {
    width: 36px; height: 36px; flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    border-radius: 50%; background: var(--border-soft); color: var(--text); text-decoration: none;
}
.page-back .icon { width: 17px; height: 17px; }
/* One chevron asset, mirrored for "back" — a second near-identical path would
   only be another thing to keep in sync. */
.page-back .flip { transform: scaleX(-1); }
.page-add {
    display: inline-flex; align-items: center; gap: 4px; flex-shrink: 0;
    padding: 7px 12px; border-radius: 12px;
    background: var(--accent-soft); color: var(--accent-dark);
    font-size: 0.82rem; font-weight: 700; text-decoration: none;
}
.page-add .icon { width: 14px; height: 14px; }

/* ===== GROUPED LIST ===== */
.grp-label { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); margin: 14px 0 8px; }
.grp-card { background: var(--card); border: 1px solid var(--border); border-radius: 16px; overflow: hidden; }
/* Divider between rows only, never above the first or below the last — a
   border on every item leaves a stray line against the card's own edge. */
.grp-item + .grp-item, .grp-card > .grp-action { border-top: 1px solid var(--border-soft); }
.grp-card .profile-row-btn {
    display: flex; align-items: center; gap: 12px; width: 100%;
    padding: 13px 14px; border: none; border-radius: 0; margin: 0;
    background: none; font: inherit; text-align: left; cursor: pointer;
}
.grp-card .profile-row-btn:hover { background: var(--border-soft); }
/* Rotating a right-chevron 90° points it down when the row is open, which is
   the same affordance the old down-arrow gave without a second icon. */
.grp-card .profile-chevron .icon { width: 17px; height: 17px; }
.grp-card .profile-row-btn.open .profile-chevron { transform: rotate(90deg); }
.grp-card .perm-sheet { margin: 0; border: none; border-top: 1px solid var(--border-soft); border-radius: 0; background: var(--bg); }
.grp-action {
    display: flex; align-items: center; justify-content: center; gap: 6px;
    padding: 13px; color: var(--accent-dark); font-size: 0.86rem; font-weight: 600; text-decoration: none;
}
.grp-action:hover { background: var(--border-soft); }
.grp-action .icon { width: 15px; height: 15px; }

/* The primary Admin — the one profile security answers to. Distinct from a
   second Admin, who can run the library but not change the lock on it. */
.sec-row-owner { display: inline-flex; align-items: center; gap: 4px; margin-top: 3px; font-size: 0.72rem; color: var(--accent-dark); font-weight: 600; }
.sec-row-owner .icon { width: 11px; height: 11px; }




/* ===== BOOK DETAIL SHEET ===== */
/* Rebuilt around one editable thing. Reading status is a segmented control at
   the top; format, owner and series are chips, because they state what the
   book is rather than offering a choice. */
.bs-section { margin-top: 16px; }
.bs-label {
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--muted);
    margin-bottom: 8px;
}

.bs-seg {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 4px;
    padding: 4px;
    background: var(--border-soft);
    border-radius: 12px;
}
.bs-seg-btn {
    min-width: 0;
    padding: 9px 4px;
    border: none;
    border-radius: 9px;
    background: none;
    font: inherit;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--muted);
    cursor: pointer;
    /* Transform and colour only — animating background-color on the track
       itself made the whole strip flash on every change. */
    transition: background 0.18s cubic-bezier(0.34, 1.4, 0.64, 1), color 0.18s ease, transform 0.18s cubic-bezier(0.34, 1.4, 0.64, 1);
}
.bs-seg-btn.on {
    background: var(--accent);
    color: #fff;
    font-weight: 700;
    box-shadow: var(--shadow-sm);
    transform: scale(1.02);
}
@media (prefers-reduced-motion: reduce) {
    .bs-seg-btn { transition: none; }
    .bs-seg-btn.on { transform: none; }
}

.bs-progress { margin-top: 12px; }
.bs-progress-read { font-size: 0.8rem; font-weight: 600; color: var(--text); margin-bottom: 6px; }
.bs-progress input[type="range"] { width: 100%; accent-color: var(--accent); }

.bs-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 16px; }
.bs-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 5px 10px;
    border-radius: 9px;
    background: var(--border-soft);
    color: var(--text);
    font-size: 0.75rem;
    font-weight: 500;
}
.bs-chip .icon { width: 13px; height: 13px; }
.bs-chip-muted { color: var(--muted); }
.bs-chip-dot {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--accent);
    color: #fff;
    font-size: 0.58rem;
    font-weight: 700;
}

.bs-blurb {
    font-size: 0.85rem;
    line-height: 1.55;
    color: var(--muted);
    /* Capped and scrollable: a long blurb otherwise pushed the admin link and
       everything else past the bottom of the sheet. */
    max-height: 9rem;
    overflow-y: auto;
    padding-right: 4px;
}
.bs-blurb-empty { font-style: italic; }

.bs-admin-link {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    width: 100%;
    margin-top: 18px;
    padding: 10px 0 2px;
    border: none;
    background: none;
    font: inherit;
    font-size: 0.75rem;
    color: var(--muted);
    text-decoration: none;
    cursor: pointer;
}
.bs-admin-link:hover { color: var(--accent-dark); }
.bs-admin-link .icon { width: 13px; height: 13px; flex-shrink: 0; }

.bs-unlock { margin-top: 10px; }
.bs-unlock-row { display: flex; gap: 8px; }
.bs-pin {
    flex: 1;
    min-width: 0;
    padding: 10px 12px;
    letter-spacing: 0.3em;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--bg);
    color: var(--text);
    font: inherit;
    font-size: 0.9rem;
}
.bs-pin-go {
    flex-shrink: 0;
    padding: 10px 16px;
    border: none;
    border-radius: 10px;
    background: var(--accent);
    color: #fff;
    font: inherit;
    font-size: 0.85rem;
    font-weight: 700;
    cursor: pointer;
}
.bs-unlock-msg { margin-top: 6px; font-size: 0.78rem; color: var(--danger-fg); }
/* Series name under the author in the detail sheet. Full width because these
   names are long — "The Faithful and The Fallen Series" is 33 characters, and
   in a chip beside Format and Owner it wrapped the whole row. */
.book-sheet-series {
    display: flex;
    align-items: center;
    gap: 5px;
    margin: 4px 0 0;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--accent-dark);
}
.book-sheet-series .icon { width: 13px; height: 13px; flex-shrink: 0; }

/* ===== BOOK EDIT FORM ===== */
/* Grouped into labelled cards like the settings pages, rather than one long
   undivided card. The controls reuse .fld/.fld-label from the Add Book form so
   the two book forms don't look like they came from different apps. */
/* Sitewide, <main> reserves 82px for the fixed tab bar. This page has no tab
   bar, so that reservation is just dead space under the Save button. */
/* Deep enough that the last control on the page — Delete this book, below
   the sticky Save — clears the platform's bottom edge band. Anything left
   sitting in that band gets its own scroll-edge treatment painted over it
   and reads as faded, which is what happened to Save before it was lifted
   and then to Delete once Save moved out of the way. */
.form-main { max-width: 640px; padding-bottom: calc(env(safe-area-inset-bottom) + 56px); }
.form-card {
    background: var(--card);
    border: 1px solid var(--border);
    border-radius: 16px;
    padding: 14px;
}
.form-card > .fld-label:first-child { margin-top: 0; }
.fld-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
/* One per row on a phone. Two native date inputs sharing ~150px each is
   tight even once they can shrink, and on iOS an empty one renders blank
   while a filled one shows a formatted date — side by side that reads as a
   broken pair rather than two fields. */
@media (max-width: 430px) { .fld-pair { grid-template-columns: 1fr; } }
/* Same as .acq-grid above — this is the pair of date fields on the edit form. */
.fld-pair > * { min-width: 0; }
.fld-pair input { min-width: 0; }
/* Backed the same way as .add-submit-wrap on the Add Book form. Without a
   background the page scrolls under a sticky button with nothing behind it,
   so form fields show through around its rounded corners. Sitting at bottom:0
   with the safe-area inset inside the padding — rather than offset from the
   bottom — means the fade reaches the screen edge instead of leaving a strip
   of scrolling content below it. */
/* Same clearance as .add-submit-wrap — see the note there. */
.form-submit {
    position: sticky;
    bottom: calc(env(safe-area-inset-bottom) + 20px);
    margin-top: 18px;
    margin-bottom: calc(env(safe-area-inset-bottom) + 20px);
    padding: 16px 0 0;
    background: linear-gradient(to bottom, transparent, var(--bg) 30%);
}

/* Owner control in the detail dialog. Shaped like the chips beside it so the
   row reads as one set of facts, but it's a real <select> for an Admin —
   reassigning a book is common enough not to need the full edit page. */
.bs-owner {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 8px 4px 6px;
    border-radius: 9px;
    background: var(--border-soft);
    border: 1px solid var(--border);
    cursor: pointer;
}
.bs-owner-select {
    border: none;
    background: none;
    font: inherit;
    font-size: 0.75rem;
    font-weight: 500;
    color: var(--text);
    cursor: pointer;
    padding: 0 2px 0 0;
    max-width: 9rem;
}
.bs-owner-select:focus { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.bs-chip-dot.is-unassigned { background: var(--border); color: var(--muted); }

/* The icon sits with the heading, not at the far edge. space-between pushed
   it 174px away, which read as an unrelated thing floating in the corner
   rather than as a label for the section. */
.bs-section-head { display: flex; align-items: center; gap: 7px; margin-bottom: 8px; }
.bs-section-head .bs-label { margin-bottom: 0; }
/* Bare icon, no chip: the chip's own padding and background made its box
   taller than the label text beside it, which is what stopped the two lining
   up. With those gone the row's align-items:center lands it exactly on the
   label's centre — measured at 0.0px offset, so no nudge is needed. */
.bs-format-ic { display: flex; align-items: center; color: var(--muted); }
.bs-format-ic .icon { width: 16px; height: 16px; }

/* Returned-book notices for Admins. Sits at the top of the list because it's
   about a book that needs attention, not a passive status line. */
.notices { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; }
.notice {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 11px 12px;
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    border-radius: 14px;
    font-size: 0.82rem;
    color: var(--text);
}
.notice-ic { display: flex; color: var(--accent-dark); flex-shrink: 0; }
.notice-ic .icon { width: 16px; height: 16px; }
.notice-text { flex: 1; min-width: 0; line-height: 1.4; }
.notice-x {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px; height: 28px;
    border: none; border-radius: 50%;
    background: none; color: var(--muted); cursor: pointer;
}
.notice-x:hover { background: var(--card); }
.notice-x .icon { width: 14px; height: 14px; }

/* Return to library, in the detail dialog. Styled as a real action rather
   than the muted admin link below it — it's something the reader does, not a
   way out to somewhere else. */
.bs-return {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    width: 100%;
    margin-top: 18px;
    padding: 11px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--card);
    font: inherit;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--text);
    cursor: pointer;
}
.bs-return:hover { background: var(--border-soft); }
.bs-return .icon { width: 15px; height: 15px; }

/* ===== WISH LIST ===== */
.wish-row {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 12px 14px;
}
.wish-row + .wish-row { border-top: 1px solid var(--border-soft); }
.wish-row form { margin: 0; }
.wish-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.wish-text strong { font-size: 0.92rem; font-weight: 600; }
.wish-text small { font-size: 0.75rem; line-height: 1.4; color: var(--muted); }
.wish-notes { font-style: italic; }
.wish-x {
    flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    width: 32px; height: 32px;
    border: none; border-radius: 50%;
    background: none; color: var(--muted); cursor: pointer;
}
.wish-x:hover { background: var(--border-soft); color: #b3261e; }
.wish-x .icon { width: 15px; height: 15px; }
.wish-empty { color: var(--muted); font-size: 0.85rem; }

/* "Bought it" — moves a wish list entry into the library. Quiet next to the
   title, since browsing the list is the common case and buying is the event. */
.wish-got {
    flex-shrink: 0;
    align-self: center;
    padding: 6px 11px;
    border-radius: 999px;
    background: var(--accent-soft);
    color: var(--accent-dark);
    font-size: 0.75rem;
    font-weight: 700;
    text-decoration: none;
}
.wish-got:hover { background: var(--accent); color: #fff; }

/* Sample notice in the detail dialog. A sample is an open question — buy it or
   drop it — so it's a prompt with an action, not a passive badge. */
.bs-sample {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-top: 16px;
    padding: 11px 12px;
    background: var(--border-soft);
    border: 1px dashed var(--border);
    border-radius: 12px;
}
.bs-sample-text { flex: 1; min-width: 0; font-size: 0.78rem; line-height: 1.4; color: var(--muted); }
.bs-sample-btn {
    flex-shrink: 0;
    padding: 7px 12px;
    border: none;
    border-radius: 999px;
    background: var(--accent);
    color: #fff;
    font: inherit;
    font-size: 0.75rem;
    font-weight: 700;
    cursor: pointer;
}
.sample-row { margin-top: 4px; }
.sample-seg { grid-template-columns: repeat(2, 1fr); }

/* Shown instead of the link-books picker once a series has members. */
.series-established {
    display: flex;
    align-items: center;
    gap: 7px;
    margin: 10px 0 0;
    font-size: 0.8rem;
    color: var(--muted);
}
.series-established .icon { width: 14px; height: 14px; flex-shrink: 0; }

/* Delete on the edit form. Quiet by default and well clear of Save — an
   irreversible action shouldn't compete for attention with the safe one. */
.form-delete { margin: 4px 0 8px; }
.form-delete button {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 7px;
    width: 100%;
    padding: 12px;
    border: 1px solid var(--border);
    border-radius: 14px;
    background: none;
    font: inherit;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--danger-fg);
    cursor: pointer;
}
.form-delete button:hover { background: var(--danger-bg); border-color: var(--danger-bg); color: #fff; }
.form-delete .icon { width: 15px; height: 15px; }
