/*
    One stylesheet, no framework. The interface is a shelf, a search box and the controls that act
    on them — small enough that a framework would be more bytes and more concepts than the thing
    it is styling.

    Colours, translucency and depth are declared once as custom properties, including a dark set
    behind prefers-color-scheme, so the material is changed in one place rather than hunted for.

    Controls are flat: a fill, a radius, and a change of fill on hover. No gradients and no outlines
    on top of fills — a button that is filled does not also need drawing round, and stacking the two
    is what makes a control look like a control from fifteen years ago.

    Surfaces are layered rather than flat: a tinted page beneath, translucent cards above it, and a
    blurred header above those. Translucency is applied only through --card-translucent and friends,
    and every one of them has an opaque fallback, because text on an unreadable background is a
    worse outcome than a missing effect.
*/

:root {
    /*
        Tells the browser which way round to paint the parts of a control it draws itself — most
        visibly the list a <select> drops down, which is rendered by the platform and not by
        anything here. Without it the platform assumes light, so in dark mode it painted a pale
        list and put our pale text on it: options that were there but could not be read.
    */
    color-scheme: light;

    /* Cloud: a cool blue-grey ground under near-opaque white cards, and slate ink. Cool light
       reads quieter than warm on a bright screen, and the green parts from it cleanly. It took
       over from Linen (oat and taupe under sheer cards), whose cards sat 1.06:1 off the page and
       whose faint ink was 2.5:1: one muddy sheet. Every line of text here is AA on a card. Chosen
       over six others on the prototype/light-themes branch. */
    --ink: #1b2129;
    --ink-soft: #4d5663;
    --ink-faint: #68717d;
    --page: #ebeef2;
    --card: #fcfdfe;
    --card-translucent: rgb(252 253 254 / 92%);
    /* The shelf's cards, a touch sheerer than a control, so the pale mesh still tells under the
       largest surfaces on the page. Much sheerer than this and they sink back into the ground,
       which is what made Linen's shelf read as one sheet. */
    --card-shelf: rgb(252 253 254 / 90%);
    /* Sheerer than a card, for the search panel: it is a place to type rather than
       something to read, so it can let more of the page through than the shelf does. */
    --card-sheer: rgb(252 253 254 / 80%);
    --card-raised: rgb(252 253 254 / 82%);
    /* The account menu: frosted glass. Sheer enough that the shelf under it shows as shapes, and
       blurred (--blur-soft) enough that none of it can be read. */
    --card-frosted: rgb(252 253 254 / 74%);
    /* The phone card's band: its jacket blurred across the top (see .entry::before). Deep jacket
       colours laid thinly on a card this pale read as grey, so on light the jacket is veiled
       toward white and saturated into a pastel, blurred to colour without shapes, and drawn
       --band-bleed past the card's edges so the blur is clipped before it can pale at them.
       The fade is measured down the card. --card-on-band is the status pill on it. */
    --band-blur: 32px;
    --band-saturate: 1.9;
    --band-warm-saturate: 1.2;
    --band-veil: rgb(255 255 255 / 40%);
    --band-opacity: 0.38;
    --band-bleed: 4rem;
    --band-fade-from: 3.4rem;
    --band-fade-to: 8.9rem;
    --card-on-band: rgb(252 253 254 / 88%);
    --line: rgb(20 30 50 / 7%);
    --line-strong: rgb(20 30 50 / 14%);
    --fill: rgb(20 30 50 / 5%);
    --fill-hover: rgb(20 30 50 / 9.5%);
    --accent: #22684d;
    --accent-hover: #1a5740;
    --accent-soft: rgb(34 104 77 / 11%);
    --accent-ink: #ffffff;
    --danger: #a2342a;
    --danger-soft: rgb(162 52 42 / 10%);
    --danger-fill: rgb(162 52 42 / 14%);
    /* The amber: for a thing that has gone quiet — neither fine nor broken, so neither the accent
       nor danger — and for a book read to the end, warm the way an old jacket is. */
    --warn: #7a5d1e;
    --warn-hover: #664e19;
    --warn-soft: rgb(212 160 84 / 18%);
    /* A status's own colour — green in hand, amber finished — for a list of statuses. Unlike the
       accent, a jacket never repaints these, so a row reads the same on every page. */
    --status-alive: #22684d;
    --status-done: #7a5d1e;
    /* How far the sticky header reaches down the page, for anything that must clear it. */
    --header-offset: 4.5rem;
    /* The page's washes: the accent and two neighbours, an amber and a blue, all as tints. Kept
       this pale on purpose — they are what the cards sit against, not a picture. Half Linen's
       strength: at full strength the fields read as blotches on a ground this light. */
    --wash-accent: rgb(34 104 77 / 8%);
    --wash-warm: rgb(212 160 84 / 7%);
    --wash-cool: rgb(86 132 176 / 7%);
    --radius: 16px;
    --radius-sm: 10px;
    --blur: saturate(180%) blur(20px);
    /* A shorter blur for the account menu. At 20px what is behind a surface dissolves into flat
       colour and the surface reads as solid whatever its tint; at 12px the shelf is still there
       as soft shapes, which is what makes the menu look like glass rather than paint. */
    --blur-soft: saturate(180%) blur(12px);
    --shadow: 0 1px 2px rgb(20 30 50 / 5%), 0 8px 24px rgb(20 30 50 / 7%);
    --shadow-lifted: 0 1px 2px rgb(20 30 50 / 6%), 0 16px 36px rgb(20 30 50 / 11%);
    /*
        Two curves. --ease is symmetric and for things that change state under the cursor: a fill
        darkening, a border colouring. --ease-out is for anything that arrives or comes to rest —
        a card rising in, the deck turning, a bar growing — because those should leave fast and
        settle slowly, the way a thing that has been set down does. Put the symmetric curve on an
        arrival and the first third of it is spent getting going, which reads as hesitation.

        One exception: a height that travels a screen or more — the book's description folding
        open and shut — takes --ease. --ease-out covers most of so long a way in its first frames,
        and what should read as a glide reads as a jump.
    */
    --ease: cubic-bezier(0.4, 0, 0.2, 1);
    --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
    --quick: 160ms;
}

/*
    The colours a book's page takes from its jacket (js/coverPalette.js) are registered as
    colours rather than left as untyped text, which is what lets them be transitioned: a bare
    custom property changes in one frame and everything built on it — the mesh's gradients most
    of all, which no rule of their own could ease — snaps with it. Registered, the values
    themselves glide and every use of them repaints along the way.

    Every one has the theme's own light value as its initial value, which is never seen: :root
    above always sets them. It is there because a registered property must have one.
*/
@property --accent { syntax: "<color>"; inherits: true; initial-value: #22684d; }
@property --accent-hover { syntax: "<color>"; inherits: true; initial-value: #1a5740; }
@property --accent-soft { syntax: "<color>"; inherits: true; initial-value: rgb(34 104 77 / 11%); }
@property --accent-ink { syntax: "<color>"; inherits: true; initial-value: #ffffff; }
@property --wash-accent { syntax: "<color>"; inherits: true; initial-value: rgb(34 104 77 / 8%); }
@property --wash-warm { syntax: "<color>"; inherits: true; initial-value: rgb(212 160 84 / 7%); }
@property --wash-cool { syntax: "<color>"; inherits: true; initial-value: rgb(86 132 176 / 7%); }
@property --mesh-hue { syntax: "<number>"; inherits: true; initial-value: 0; }
@property --mesh-glow { syntax: "<number>"; inherits: true; initial-value: 1; }

/*
    The mesh takes a shade of its own on each of the shelf's tabs. Its four fields of colour (on
    body) keep their places and their lightness; only their hue moves, by a few degrees round
    the wheel (oklch, so a shift is as strong in one colour as another), and switching tab glides
    from one shade to the next, so the page changes with the choice rather than only the list
    under it. All has a shade too (see below); off the shelf there are no tabs, and it drifts
    home.
*/
:root {
    transition:
        --mesh-hue 2200ms var(--ease-out),
        --mesh-glow 2200ms var(--ease-out);
}

/*
    All has a shade of its own too. Left unturned it was the one tab whose fields kept their
    plain colours, and in the dark those read as the dullest of the set — next to its
    neighbours, nearly black. It turns a little toward Reading's warmth and its fields glow a
    little stronger (--mesh-glow scales how opaque they are), so it is of a piece with the rest
    rather than the gap between them. Off the shelf the mesh still drifts home to no shift at all.
*/
:root:has(.chip--on[data-status="All"]) { --mesh-hue: 14; --mesh-glow: 1.45; }
:root:has(.chip--on[data-status="Reading"]) { --mesh-hue: 28; }
:root:has(.chip--on[data-status="WantToRead"]) { --mesh-hue: 60; }
:root:has(.chip--on[data-status="Finished"]) { --mesh-hue: -38; }
:root:has(.chip--on[data-status="OnHold"]) { --mesh-hue: -18; }
:root:has(.chip--on[data-status="Dropped"]) { --mesh-hue: 95; }

/*
    Only while a page is wearing a jacket, and only from the frame after it has put on what it
    already knew — so a page revisited is in its colours at once, and it is the colours arriving
    afresh, or the theme being switched under them, that glide. Long, because the arrival is
    the whole page changing colour, and a page that changes colour quickly looks like a fault.
*/
:root[data-jacket] {
    transition:
        --accent 900ms var(--ease-out),
        --accent-hover 900ms var(--ease-out),
        --accent-soft 900ms var(--ease-out),
        --accent-ink 900ms var(--ease-out),
        --wash-accent 1200ms var(--ease-out),
        --wash-warm 1200ms var(--ease-out),
        --wash-cool 1200ms var(--ease-out),
        --mesh-hue 2200ms var(--ease-out),
        --mesh-glow 2200ms var(--ease-out);
}

/*
    The dark set, twice over: once for a reader whose system is dark and who has not said
    otherwise, once for a reader who chose dark here whatever their system says. The tokens are
    the same lines both times, so the two can never drift; a choice of light is the first rule's
    :not() and nothing more. The choice is stamped on <html> as data-theme before first paint
    — see index.html — so the page never flashes the other theme on the way in.
*/
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        color-scheme: dark;

        --ink: #ededea;
        --ink-soft: #a6aba8;
        --ink-faint: #7c817f;
        --page: #111314;
        --card: #1c1f21;
        --card-translucent: rgb(35 39 41 / 66%);
        --card-shelf: rgb(35 39 41 / 41%);
        --card-sheer: rgb(35 39 41 / 42%);
        --card-raised: rgb(28 31 33 / 72%);
        --card-frosted: rgb(28 31 33 / 50%);
        --band-blur: 18px;
        --band-saturate: 1.3;
        --band-warm-saturate: 0.7;
        --band-veil: transparent;
        --band-opacity: 0.5;
        --band-bleed: 0rem;
        --band-fade-from: 4.875rem;
        --band-fade-to: 9.75rem;
        --card-on-band: rgb(28 31 33 / 72%);
        --line: rgb(255 255 255 / 8%);
        --line-strong: rgb(255 255 255 / 16%);
        --fill: rgb(255 255 255 / 8%);
        --fill-hover: rgb(255 255 255 / 14%);
        --accent: #8ec5a4;
        --accent-hover: #a3d4b6;
        --accent-soft: rgb(142 197 164 / 15%);
        --accent-ink: #0f2318;
        --danger: #e28a7c;
        --danger-soft: rgb(226 138 124 / 14%);
        --danger-fill: rgb(226 138 124 / 18%);
        --warn: #e6b260;
        --warn-hover: #f0c47c;
        --warn-soft: rgb(230 178 96 / 16%);
        --status-alive: #8ec5a4;
        --status-done: #e6b260;
        --wash-accent: rgb(142 197 164 / 17%);
        --wash-warm: rgb(230 178 96 / 13%);
        --wash-cool: rgb(120 160 210 / 13%);
        --shadow: 0 1px 2px rgb(0 0 0 / 40%), 0 10px 26px rgb(0 0 0 / 32%);
        --shadow-lifted: 0 1px 2px rgb(0 0 0 / 45%), 0 16px 36px rgb(0 0 0 / 42%);
    }
}

:root[data-theme="dark"] {
    color-scheme: dark;

    --ink: #ededea;
    --ink-soft: #a6aba8;
    --ink-faint: #7c817f;
    --page: #111314;
    --card: #1c1f21;
    --card-translucent: rgb(35 39 41 / 66%);
    --card-shelf: rgb(35 39 41 / 41%);
    --card-sheer: rgb(35 39 41 / 42%);
    --card-raised: rgb(28 31 33 / 72%);
    --card-frosted: rgb(28 31 33 / 50%);
    --band-blur: 18px;
    --band-saturate: 1.3;
    --band-warm-saturate: 0.7;
    --band-veil: transparent;
    --band-opacity: 0.5;
    --band-bleed: 0rem;
    --band-fade-from: 4.875rem;
    --band-fade-to: 9.75rem;
    --card-on-band: rgb(28 31 33 / 72%);
    --line: rgb(255 255 255 / 8%);
    --line-strong: rgb(255 255 255 / 16%);
    --fill: rgb(255 255 255 / 8%);
    --fill-hover: rgb(255 255 255 / 14%);
    --accent: #8ec5a4;
    --accent-hover: #a3d4b6;
    --accent-soft: rgb(142 197 164 / 15%);
    --accent-ink: #0f2318;
    --danger: #e28a7c;
    --danger-soft: rgb(226 138 124 / 14%);
    --danger-fill: rgb(226 138 124 / 18%);
    --warn: #e6b260;
    --warn-hover: #f0c47c;
    --warn-soft: rgb(230 178 96 / 16%);
    --status-alive: #8ec5a4;
    --status-done: #e6b260;
    --wash-accent: rgb(142 197 164 / 17%);
    --wash-warm: rgb(230 178 96 / 13%);
    --wash-cool: rgb(120 160 210 / 13%);
    --shadow: 0 1px 2px rgb(0 0 0 / 40%), 0 10px 26px rgb(0 0 0 / 32%);
    --shadow-lifted: 0 1px 2px rgb(0 0 0 / 45%), 0 16px 36px rgb(0 0 0 / 42%);
}

*,
*::before,
*::after {
    box-sizing: border-box;
}

/*
    Nothing is ever wider than the screen. The body's clip below is for the hero; this one is for
    the viewport, which on a phone is the thing that matters: anything that pokes past the right
    edge — a tooltip laid out at opacity 0 was enough — and iOS lets the whole page pan across to
    it, sliding the sticky header out of line and leaving a strip of nothing beside it. The page
    that let it happen is fixed too, but a stray pixel should not be able to do that again.
*/
html {
    overflow-x: clip;
    /*
        The gutter is held whether or not there is anything to scroll, so the page does not go
        15px narrower the moment it grows past the window. Without it the bar arriving is a
        relayout of everything on screen -- the column is centred, so all of it steps left --
        and the mesh is sized to the viewport, so it repaints at the new width in the same
        frame. It lands as a hitch, and it lands just as a shelf or a description has finished
        arriving, which is the worst moment to ask the eye to start again.

        Phones are not affected either way: their scrollbars are drawn over the page and take no
        width, so there is no gutter to hold and this does nothing.
    */
    scrollbar-gutter: stable;
}

body {
    margin: 0;
    /* Lets the welcome screen's hero break out of the column to the full width of the window.
       `clip` rather than `hidden` so the vertical axis stays visible and the header keeps
       sticking. */
    overflow-x: clip;
    font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
    line-height: 1.55;
    color: var(--ink);
    background: var(--page);
    /*
        Four broad fields of colour blended into one soft mesh — green, amber, blue, green — so the
        translucent surfaces above have something to be translucent against wherever they sit on
        the page, not only in the top corners. Fixed, so it does not slide about as the shelf
        scrolls. Each field's hue is shifted by --mesh-hue, the shade of the shelf tab that is open
        (see there); on All, and off the shelf, it is no shift at all.

        On the body, and not on a fixed element of its own — that was tried. The body's background
        is the canvas: it is painted under every part of the screen the browser shows the page
        through, including the strip behind a phone browser's toolbar as it grows and shrinks. A
        fixed element is not; Safari on iPhone clips it to the layout viewport, which is the screen
        less the toolbar, so the mesh stopped in a hard edge at the toolbar's top and the edge rode
        up and down with it. Safari also ignores the attachment and lets the mesh scroll with the
        page, which is a smaller thing to live with than an edge. Chrome composites a fixed root
        background, so it costs nothing on the scroll.

        The two fields at the top are placed a length down from the top rather than a share of
        the height. Where the mesh is fixed the two are the same thing; where Safari scrolls it,
        a share is a share of the whole page, and on a long shelf — All, most of all — 12% of it
        lies screens below the top, which was left all but bare.

        KEEP IN STEP with .splash--settling, which paints this same canvas over the page it is
        covering so that covering the page does not cover the mesh.
    */
    background-image:
        radial-gradient(56rem 40rem at 12% 6rem, oklch(from var(--wash-accent) l c calc(h + var(--mesh-hue)) / calc(alpha * var(--mesh-glow))), transparent 66%),
        radial-gradient(52rem 38rem at 88% 2rem, oklch(from var(--wash-warm) l c calc(h + var(--mesh-hue)) / calc(alpha * var(--mesh-glow))), transparent 66%),
        radial-gradient(56rem 40rem at 76% 96%, oklch(from var(--wash-cool) l c calc(h + var(--mesh-hue)) / calc(alpha * var(--mesh-glow))), transparent 66%),
        radial-gradient(48rem 34rem at 14% 100%, oklch(from var(--wash-accent) l c calc(h + var(--mesh-hue)) / calc(alpha * var(--mesh-glow))), transparent 66%);
    background-attachment: fixed;
    min-height: 100vh;
    -webkit-font-smoothing: antialiased;
}

h1 {
    font-size: 1.75rem;
    margin: 0;
    letter-spacing: -0.025em;
    font-weight: 700;
}

h2 {
    font-size: 1.15rem;
    margin: 0 0 1rem;
    letter-spacing: -0.015em;
    font-weight: 650;
}

h3 {
    font-size: 1rem;
    margin: 0;
    letter-spacing: -0.01em;
    font-weight: 650;
}

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

.muted {
    color: var(--ink-soft);
}

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

/*
    The loading curtain over the welcome screen: opaque, the page's own colour, holding the boot
    animation while the deck's jackets arrive behind it.

    A curtain rather than a switch, because the screen underneath is finished the whole time. When
    the jackets are in, this fades and what is already there is simply uncovered — where swapping
    one for the other could only ever be a cut.

    Full width of the window, like the poster it covers: the reading column it sits in is narrower
    than the hero, so matching the column would leave the poster showing down both sides.
*/
.welcome {
    position: relative;
}

.welcome__curtain {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 100vw;
    height: 100vh;
    z-index: 5;
    display: grid;
    place-items: center;
    background: var(--page);
    transition: opacity 500ms var(--ease-out);
}

.welcome__curtain--lifted {
    opacity: 0;
    /* Gone as far as the cursor is concerned the moment it starts to lift, so the sign-in button
       underneath is never a click that lands on nothing. */
    pointer-events: none;
}

/*
    The welcome arrives in order — greeting, title, pitch, the way in, then the deck — instead of
    the whole page appearing at once behind a curtain that has just lifted. Each is a few frames
    behind the one above it, which is enough to read as a sequence without anybody waiting on it.

    It is keyed to the curtain lifting rather than to the component mounting, because the page is
    built and painted long before it is shown: run on mount, the whole thing would play behind the
    curtain and be over before anyone saw it.
*/
@keyframes rise {
    from { opacity: 0; transform: translateY(0.65rem); }
    to { opacity: 1; transform: none; }
}

.welcome--shown .hero__greeting,
.welcome--shown .hero__title,
.welcome--shown .hero__pitch,
.welcome--shown .hero__actions,
.welcome--shown .flow,
.welcome--shown .pitch {
    animation: rise 560ms var(--ease-out) both;
}

.welcome--shown .hero__title { animation-delay: 70ms; }
.welcome--shown .hero__pitch { animation-delay: 140ms; }
.welcome--shown .hero__actions { animation-delay: 210ms; }
.welcome--shown .flow { animation-delay: 180ms; }
.welcome--shown .pitch { animation-delay: 300ms; }

/* Header ------------------------------------------------------------------ */

header {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 0.7rem 1.5rem;
    border-bottom: 1px solid var(--line);
    background: var(--card);
}

/*
    The brand is an eyebrow, not a headline: small capitals, spaced, in the faint ink. The page's
    own heading is the loud thing on the screen, and a wordmark set at the same weight was
    competing with it from a bar that is on every page.
*/
header > a:first-child {
    font-size: 0.72rem;
    font-weight: 650;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    text-decoration: none;
    color: var(--ink-faint);
    transition: color var(--quick) var(--ease);
}

header > a:first-child:hover {
    color: var(--ink);
}

/*
    The year's goal, in the bar's free middle: the figure and a short bar, quiet as the brand
    beside it until the cursor asks. A button, so it can be tapped for the form; the menu that
    opens is the account menu's, anchored under the badge.
*/
.goal {
    position: relative;
    display: flex;
    margin-left: auto;
}

.goal__badge {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.3rem 0.6rem;
    border-radius: 999px;
    background: transparent;
    color: var(--ink-soft);
    font-size: 0.82rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    transition: background-color var(--quick) var(--ease), color var(--quick) var(--ease);
}

.goal__badge:hover:not(:disabled),
.goal__badge[aria-expanded="true"] {
    background: var(--fill);
    color: var(--ink);
}

.goal__of {
    font-weight: 500;
    color: var(--ink-faint);
}

.goal__track {
    width: 5.5rem;
    height: 4px;
    border-radius: 999px;
    background: var(--fill-hover);
    overflow: hidden;
}

.goal__fill {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: var(--accent);
    transition: width 560ms var(--ease-out);
}

.goal__menu {
    position: absolute;
    top: calc(100% + 0.5rem);
    right: 0;
    z-index: 2;
    width: 16rem;
    padding: 0.9rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--card);
    box-shadow: var(--shadow-lifted);
    animation: unfold 240ms var(--ease-out) backwards;
}

.goal__heading {
    margin: 0 0 0.7rem;
    font-size: 0.9rem;
    color: var(--ink-soft);
}

.goal__heading strong {
    color: var(--ink);
    font-weight: 650;
}

.goal__pace {
    display: block;
    margin-top: 0.15rem;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--accent);
}

.goal__form {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0.3rem 0.5rem;
    align-items: center;
}

.goal__form label {
    grid-column: 1 / -1;
    font-size: 0.8rem;
    color: var(--ink-soft);
}

.goal__form input {
    width: 100%;
}

.goal__clear {
    margin-top: 0.5rem;
    padding-inline: 0;
    font-size: 0.85rem;
}

/* An import under way, as a small live pill in the bar: a pulse and the count. */
.import-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    margin-left: auto;
    padding: 0.25rem 0.7rem;
    border-radius: 999px;
    background: var(--accent-soft);
    color: var(--accent);
    font-size: 0.8rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    text-decoration: none;
    white-space: nowrap;
}

.import-badge + .goal {
    margin-left: 0;
}

.import-badge__dot {
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
    background: currentColor;
    animation: pulse 1.6s ease-in-out infinite;
}

@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.35; }
}

@media (prefers-reduced-motion: reduce) {
    .import-badge__dot {
        animation: none;
    }

    .book__text,
    .book__about p {
        transition: none;
    }
}

/*
    The Book page: one column, the width of a comfortable line, the same on a phone and on a
    desk. The book half sits at the top like the head of a shelf card grown up — a cover the
    size of a thumb beside the title — and the reading half follows.
*/
.book {
    max-width: 40rem;
    margin: 0 auto;
}

/*
    The way back is one chevron and no word — the word is there for a screen reader — a 2rem
    square as quiet as a link-button: nothing at rest, the fill when pointed at. It was an
    outlined pill, "← Back", on a line of its own above the head; the page has no outlined
    control now but Read more. On a wide screen it hangs in the left margin outside the column,
    level with the title, the way a running head sits outside a text block (see the min-width
    rule below); the column itself starts at the cover. Where there is no margin to hang in it
    stands in the flow above the head, its glyph on the cover's edge.
*/
.book {
    position: relative;
}

.book__back {
    display: inline-grid;
    place-items: center;
    width: 2rem;
    height: 2rem;
    margin: -0.3rem 0 0.2rem -0.45rem;
    border-radius: var(--radius-sm);
    color: var(--ink-soft);
    transition: background var(--quick) var(--ease), color var(--quick) var(--ease);
}

.book__back svg {
    width: 1.05rem;
    height: 1.05rem;
}

.book__back:hover {
    background: var(--fill);
    color: var(--ink);
}

/* Room in the margin for it: the column plus the arrow and its gap on either side, and the page's
   own padding outside that. The gap is 1.4rem: at 0.6rem the chevron crowded the cover. */
@media (min-width: 49rem) {
    .book__back {
        position: absolute;
        top: -0.1rem;
        right: calc(100% + 1.4rem);
        margin: 0;
    }
}

/*
    The page is built in two arrivals — the entry from Library, then the book's details from
    Catalog — and each part rises into place as it comes, a beat apart, the way the shelf's cards
    do. Backwards rather than both, for the same reason as the cards: a kept final frame keeps
    its transform.
*/
.book__head,
.book__edition,
.book > .reading {
    animation: rise 420ms var(--ease-out) backwards;
}

.book__edition { animation-delay: 70ms; }
.book > .reading { animation-delay: 140ms; }

/*
    Catalog's part (the categories, the description) lands on a page that is already standing,
    and dropped in whole it shoved everything under it down by its height in one frame. So each
    has a box that stands empty from the first frame and opens to its content's height once
    Catalog answers — 0fr to 1fr, which glides where a height to auto would not — carrying the
    edition line and the reading half down with it. The words fade in once the room is mostly
    there: fading rather than rising, since a rise inside a box that is itself opening reads as
    two motions.

    The inside is clipped while the box opens, and stops being clipped once it is open, so the
    Read more pill's focus ring is not cut off at the box's edge.
*/
.book__reveal {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 420ms var(--ease-out);
}

.book__reveal--open {
    grid-template-rows: 1fr;
}

.book__reveal-inner {
    min-height: 0;
    overflow: hidden;
}

.book__reveal--open .book__reveal-inner {
    overflow: visible;
    transition: overflow 0s 420ms allow-discrete;
}

.book__categories,
.book__about,
.book__note {
    animation: fade 380ms var(--ease-out) 160ms backwards;
}

@keyframes fade {
    from { opacity: 0; }
    to { opacity: 1; }
}

.book__head {
    display: flex;
    gap: 1.1rem;
    align-items: flex-start;
}

/* Tall enough to stand beside the title, the authors and two rows of categories, rather than
   ending halfway down them. A phone sets its own size — see the phone rules. */
.book__cover {
    flex: 0 0 auto;
    width: 6rem;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: var(--shadow);
}

.book__cover img {
    display: block;
    width: 100%;
}

.book__ident {
    flex: 1 1 0;
    min-width: 0;
}

/* The status at the head's right end, level with the title, as it is on the card. */
.book__status {
    flex: 0 0 auto;
    margin-left: auto;
}

.book__title {
    font-size: 1.55rem;
    line-height: 1.15;
    text-wrap: balance;
}

.book__authors {
    margin: 0.3rem 0 0;
    font-size: 1rem;
    color: var(--ink-soft);
}

/* Where a provider places the book: chips, but not the shelf's filter chips — these are read,
   not pressed, so they sit on a quiet fill rather than on nothing. */
.book__categories {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin: 0.6rem 0 0;
    padding: 0;
    list-style: none;
}

.chip--tag {
    padding: 0.2rem 0.65rem;
    background: var(--fill);
    font-size: 0.78rem;
}

/* The description reads like a jacket flap: a serif, a measure, room between paragraphs. */
.book__about {
    /* The fold's timing, said once: the box, the clamp and the paragraphs' space move together. */
    --fold-open: 560ms;
    --fold-shut: 460ms;
    --fold-curve: var(--ease);

    max-width: 62ch;
    margin: 1.1rem 0 0;
    font-family: Georgia, "Times New Roman", serif;
    font-size: 0.98rem;
    line-height: 1.55;
}

.book__about p {
    margin: 0 0 0.7rem;
    transition:
        margin var(--fold-open) var(--fold-curve),
        text-indent var(--fold-open) var(--fold-curve);
}

.book__about p:last-child {
    margin-bottom: 0;
}

/*
    Folded: seven lines, whatever the book, cut with an ellipsis wherever the seventh line ends —
    mid-sentence, usually — so every book's page has the same shape down to Read more. The
    paragraphs run on without space between them while folded, marked by an indent instead,
    because a gap that fell inside the seven lines would make one book's fold taller than
    another's; unfolded, they get their space back.

    Read more opens the fold rather than replacing it: the box is seven lines tall folded and as
    tall as the text open, and glides between the two (interpolate-size is what lets a height
    travel to auto; a browser without it snaps, which is what it did before). The clamp itself
    cannot glide, so it is timed: lifted at once on the way open, so the text under the fold is
    there for the box to reveal, and put back only once the box has closed on the way shut, so
    the text is never cut to seven lines with the box still tall around it. The paragraphs'
    space and indent travel with the height, so the whole passage settles rather than jumps.

    The same display, folded and open: the clamp needs -webkit-box, and a box that changed its
    display mid-glide would not glide.
*/
.book__text {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    overflow: hidden;
    interpolate-size: allow-keywords;
    height: auto;
    transition:
        height var(--fold-open) var(--fold-curve),
        -webkit-line-clamp 0s allow-discrete,
        line-clamp 0s allow-discrete;
}

.book__about--folded .book__text {
    -webkit-line-clamp: 7;
    line-clamp: 7;
    height: 7lh;
    transition:
        height var(--fold-shut) var(--fold-curve),
        -webkit-line-clamp 0s var(--fold-shut) allow-discrete,
        line-clamp 0s var(--fold-shut) allow-discrete;
}

.book__about--folded p {
    margin: 0;
    transition:
        margin var(--fold-shut) var(--fold-curve),
        text-indent var(--fold-shut) var(--fold-curve);
}

.book__about--folded p + p {
    text-indent: 1.25em;
}

/* Read more, and Show less: a small outlined pill, visibly a button at rest and not only under the pointer,
   since it sits in running text where a bare word would pass for part of it. */
.book__more {
    margin-top: 0.2rem;
    padding: 0.3rem 0.85rem;
    /* One width on every page and for both labels: "Read more" and "Show less" are not the same
       length, and a pill that resized with them shifted under the pointer each time it was
       pressed, and sat at a different width on every book. */
    min-width: 7.5rem;
    text-align: center;
    border: 1px solid var(--line-strong);
    border-radius: 999px;
    background: var(--fill);
    color: var(--ink);
    font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
    font-size: 0.8rem;
    font-weight: 600;
}

.book__more:hover:not(:disabled) {
    background: var(--fill-hover);
    border-color: var(--ink-faint);
}

.book__note {
    margin: 1.1rem 0 0;
}

.book__edition {
    display: flex;
    flex-wrap: wrap;
    gap: 0.15rem 0.45rem;
    margin: 0.9rem 0 0;
    font-size: 0.82rem;
    color: var(--ink-faint);
    font-variant-numeric: tabular-nums;
}

.book__edition a {
    color: var(--ink-soft);
    text-decoration: none;
}

.book__edition a:hover {
    color: var(--accent);
    text-decoration: underline;
}

.book__edition:empty {
    display: none;
}

/*
    The reading half: a column, the pace stripe first — its top rule is the line between the
    book and the reading — the device line under it, and every panel full width after them.
    The status is not here: it is up in the head, level with the title, the same place it has
    on the card.
*/
.reading {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    row-gap: 0.6rem;
    align-items: start;
    margin-top: 1.6rem;
}

.reading__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.2rem;
}

/* On the card the button sits in a row of link-buttons and is inset to match them; here it leads. */
.reading__actions .button--log {
    margin-left: 0;
}

/*
    The pace stripe: four figures ruled above and below, each with its label in small capitals
    beneath — the same voice as the progress figure, one size down, because these are the
    reader's numbers about the reader's reading. Two across on a phone.
*/
.stripe {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    margin: 0.3rem 0 0;
    border-block: 1px solid var(--line);
}

.stripe__figure {
    display: flex;
    flex-direction: column-reverse;
    min-width: 0;
    margin: 0;
    padding: 0.6rem 0.7rem;
    border-left: 1px solid var(--line);
}

.stripe__figure:first-child {
    padding-left: 0;
    border-left: 0;
}

.stripe__figure dd {
    margin: 0;
}

.stripe__figure strong {
    display: block;
    font-size: 1.2rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
}

.stripe__accent {
    color: var(--accent);
}

.stripe__figure dt {
    margin-top: 0.25rem;
    font-size: 0.66rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    line-height: 1.25;
    color: var(--ink-faint);
}

/* Nothing to report is not a number, and is not weighted like one. */
.stripe__figure--none strong {
    color: var(--ink-faint);
    font-weight: 500;
}

@media (max-width: 560px) {
    .stripe {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .stripe__figure:nth-child(3) {
        padding-left: 0;
        border-left: 0;
    }

    .stripe__figure:nth-child(n + 3) {
        border-top: 1px solid var(--line);
    }
}

/*
    Pages a day, as bars. Thin, rounded at the top, on the accent — the reading's own colour —
    with a day of nothing as a stub in the grid's grey, so the row is twenty-one days and not a
    few bars with holes. One line marks the maximum and names it; nothing else is labelled,
    because a habit is read as a shape, and each bar says its own figure when asked.
*/
.chart {
    margin: 0.9rem 0 0;
}

.chart__eyebrow {
    margin: 0 0 0.55rem;
}

.chart__eyebrow span {
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
    color: var(--ink-soft);
}

.chart__plot {
    position: relative;
    height: 3.25rem;
    border-top: 1px solid var(--line);
    border-bottom: 1px solid var(--line-strong);
    /* Sideways, the plot clips: a tip hanging past its edge — even one at opacity 0, which
       still has a width — hung past the page on a phone, and iOS let the page pan across to it,
       sliding the sticky header out of line with a strip of nothing at its right. Upward stays
       open, because that is where the tips and the maximum's label are. */
    overflow-x: clip;
    overflow-y: visible;
}

.chart__max {
    position: absolute;
    right: 0;
    top: -0.05rem;
    transform: translateY(-100%);
    font-size: 0.65rem;
    font-variant-numeric: tabular-nums;
    color: var(--ink-faint);
}

.chart__bars {
    display: flex;
    align-items: flex-end;
    gap: 3px;
    height: 100%;
    margin: 0;
    padding: 0.25rem 0 0;
    list-style: none;
}

.chart__bar {
    position: relative;
    flex: 1 1 0;
    min-width: 0;
    height: max(var(--h), 2px);
    border-radius: 2px 2px 0 0;
    background: var(--accent);
    opacity: 0.85;
    cursor: default;
    outline-offset: 2px;
}

.chart__bar--none {
    background: var(--fill-hover);
    opacity: 1;
}

.chart__bar:hover,
.chart__bar:focus-visible {
    opacity: 1;
}

/* The bar's day and figure, above it, only while it is pointed at or focused. */
.chart__tip {
    position: absolute;
    left: 50%;
    bottom: calc(100% + 0.35rem);
    transform: translateX(-50%);
    padding: 0.2rem 0.5rem;
    border-radius: var(--radius-sm);
    background: var(--ink);
    color: var(--page);
    font-size: 0.72rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transition: opacity 120ms var(--ease);
    z-index: 1;
}

.chart__bar:hover .chart__tip,
.chart__bar:focus-visible .chart__tip {
    opacity: 1;
}

/* A tip centred on a bar near either end would hang past the plot, and the plot now clips it.
   The few bars at each end anchor their tip to their outer edge instead — four, because a tip
   is about that many bars wide on a phone. */
.chart__bar:nth-child(-n + 4) .chart__tip {
    left: 0;
    transform: none;
}

.chart__bar:nth-last-child(-n + 4) .chart__tip {
    left: auto;
    right: 0;
    transform: none;
}

.chart__axis {
    display: flex;
    justify-content: space-between;
    margin-top: 0.25rem;
    font-size: 0.65rem;
    color: var(--ink-faint);
}

.chart .hint {
    margin-top: 0.5rem;
}

/* The things done once, at the foot, past everything read daily. */
.reading__foot {
    display: flex;
    flex-wrap: wrap;
    gap: 0.15rem;
    margin-top: 1.2rem;
    padding-top: 0.8rem;
    border-top: 1px solid var(--line);
    font-size: 0.85rem;
}

/* Pulled out to the page's edge, so the first of them lines up with the text above. */
.reading__foot > :first-child {
    margin-left: -0.6rem;
}

.reading .panel {
    margin-top: 0;
}

/* The cover and title on a shelf card are links now, but not links that look like the web:
   the title keeps its colour and only underlines when asked. */
.entry__title a {
    color: inherit;
    text-decoration: none;
}

.entry__title a:hover,
.entry__title a:focus-visible {
    text-decoration: underline;
    text-underline-offset: 0.15em;
}

a.entry__cover {
    display: block;
}

/* The reader, as one round initial at the end of the bar. */
.account {
    position: relative;
    display: flex;
    flex: 0 0 auto;
}

.account__monogram {
    width: 2rem;
    height: 2rem;
    padding: 0;
    border-radius: 50%;
    background: var(--accent-soft);
    color: var(--accent);
    font-size: 0.9rem;
    font-weight: 700;
    display: inline-grid;
    place-items: center;
}

.account__monogram:hover:not(:disabled),
.account__monogram[aria-expanded="true"] {
    background: var(--accent);
    color: var(--accent-ink);
}

/* Catches the tap that closes the menu. Below the menu, above everything else. */
.account__scrim {
    position: fixed;
    inset: 0;
    z-index: 1;
    padding: 0;
    border-radius: 0;
    background: transparent;
    cursor: default;
}

.account__scrim:hover:not(:disabled) {
    background: transparent;
}

.account__menu {
    position: absolute;
    top: calc(100% + 0.5rem);
    right: 0;
    z-index: 2;
    min-width: 13.5rem;
    padding: 0.5rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--card);
    box-shadow: var(--shadow-lifted);
    animation: unfold 240ms var(--ease-out) backwards;
}

.account__who {
    margin: 0;
    padding: 0.45rem 0.55rem 0.65rem;
    font-size: 0.88rem;
    color: var(--ink-soft);
    overflow-wrap: anywhere;
}

.account__who strong {
    color: var(--ink);
    font-weight: 600;
}

/*
    The menu's rows — a page and the one action — set alike: a quiet row that fills on hover, as
    the rows of a menu do, rather than one underlined link over one filled button.
*/
.account__row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.6rem;
    width: 100%;
    margin: 0;
    padding: 0.5rem 0.55rem;
    border-radius: 8px;
    background: transparent;
    color: var(--ink);
    font-size: 0.92rem;
    font-weight: 500;
    text-align: left;
    text-decoration: none;
    transition: background-color var(--quick) var(--ease);
}

.account__row:hover:not(:disabled) {
    background: var(--fill);
    text-decoration: none;
}

/* Dark mode, as a row like the others with a switch at its end. */
.account__theme {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.6rem;
    margin-top: 0.35rem;
    padding: 0.5rem 0.55rem 0.3rem;
    border-top: 1px solid var(--line);
    font-size: 0.92rem;
    font-weight: 500;
}

/*
    A switch of the kind a phone has: a pill track with a knob that slides to the far end and
    the track filling with the accent when it is on. A button underneath, so it is focusable and
    pressable like one; role="switch" and aria-checked say what it is.
*/
.switch {
    position: relative;
    flex: 0 0 auto;
    width: 2.6rem;
    height: 1.55rem;
    padding: 0;
    border-radius: 999px;
    background: var(--fill-hover);
    transition: background-color var(--quick) var(--ease);
}

.switch:hover:not(:disabled) {
    background: var(--line-strong);
}

.switch[aria-checked="true"],
.switch[aria-checked="true"]:hover:not(:disabled) {
    background: var(--accent);
}

.switch__knob {
    position: absolute;
    top: 0.15rem;
    left: 0.15rem;
    width: 1.25rem;
    height: 1.25rem;
    border-radius: 50%;
    background: #ffffff;
    box-shadow: 0 1px 3px rgb(0 0 0 / 30%);
    transition: transform 200ms var(--ease-out);
}

.switch[aria-checked="true"] .switch__knob {
    transform: translateX(1.05rem);
}

@media (prefers-reduced-motion: reduce) {
    .switch__knob {
        transition: none;
    }
}

/* Signing out sits apart from the pages, under a rule. */
.account__out {
    margin-top: 0.35rem;
    padding-top: 0.4rem;
    border-top: 1px solid var(--line);
}

header input {
    max-width: 9rem;
}

main {
    --main-pad-top: 1.75rem;

    padding: var(--main-pad-top) 1.25rem 5rem;
    max-width: 52rem;
    margin: 0 auto;
}

/* Controls ---------------------------------------------------------------- */

button,
input,
select,
textarea {
    font: inherit;
    color: inherit;
}

input,
select,
textarea {
    padding: 0.45rem 0.7rem;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-sm);
    background: var(--card);
    min-width: 0;
    transition:
        border-color var(--quick) var(--ease),
        box-shadow var(--quick) var(--ease),
        background-color var(--quick) var(--ease),
        color var(--quick) var(--ease);
}

input::placeholder,
textarea::placeholder {
    color: var(--ink-faint);
}

/*
    Safari on iOS draws a date field as a platform control sized to the widest date it could ever
    hold, and that intrinsic width beats `width: 100%` -- the box then runs past the card it sits
    in. Dropping the platform appearance makes it an ordinary field again, sized by whatever holds
    it, and the value inside is set from the left like every other figure in these forms.
*/
input[type="date"] {
    -webkit-appearance: none;
    appearance: none;
    max-width: 100%;
    text-align: left;
}

input[type="date"]::-webkit-date-and-time-value {
    min-width: 0;
    text-align: left;
}

/*
    Opaque, and stated on the option itself. The dropped-down list is drawn by the platform outside
    the page, where a translucent fill has nothing behind it to be translucent against.
*/
select {
    background-color: var(--card);
}

select option {
    background-color: var(--card);
    color: var(--ink);
}

input:hover:not(:disabled),
select:hover:not(:disabled),
textarea:hover:not(:disabled) {
    border-color: var(--ink-faint);
}

input:focus-visible,
select:focus-visible,
textarea:focus-visible,
button:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/*
    FocusOnNavigate moves focus to the page's h1 on every navigation, so a screen reader starts
    reading at the new page rather than wherever the last one left off. It marks that heading
    tabindex="-1" to be able to, and the browser then draws a focus ring round the whole width of
    the heading — a white box appearing over the page a moment after it loads, for a reason nobody
    looking at it can see. Nothing here that carries tabindex="-1" can be reached by keyboard, so
    there is no ring worth keeping.
*/
[tabindex="-1"]:focus,
[tabindex="-1"]:focus-visible {
    outline: none;
}

/*
    The default button: a soft fill and nothing else. It reads as pressable without competing with
    the one filled action beside it, which is what .button--primary is for.
*/
button {
    padding: 0.45rem 0.95rem;
    border: none;
    border-radius: var(--radius-sm);
    background: var(--fill);
    color: var(--ink);
    cursor: pointer;
    font-weight: 550;
    transition:
        background-color var(--quick) var(--ease),
        color var(--quick) var(--ease),
        transform var(--quick) var(--ease);
}

button:hover:not(:disabled) {
    background: var(--fill-hover);
}

button:active:not(:disabled) {
    transform: scale(0.97);
}

button:disabled {
    opacity: 0.45;
    cursor: default;
}

/* The one thing you came to this surface to do. */
.button--primary {
    background: var(--accent);
    color: var(--accent-ink);
}

.button--primary:hover:not(:disabled) {
    background: var(--accent-hover);
}

.button--large {
    padding: 0.7rem 1.5rem;
    font-size: 1.05rem;
    border-radius: 12px;
    font-weight: 600;
}

button.danger {
    background: var(--danger-fill);
    color: var(--danger);
}

button.danger:hover:not(:disabled) {
    background: var(--danger);
    color: var(--card);
}

/*
    A quiet action. Underlining these turned a row of four into a thicket, so they are text until
    pointed at and take a fill when they are.
*/
.link-button {
    padding: 0.3rem 0.6rem;
    background: transparent;
    color: var(--ink-soft);
    font-weight: 500;
}

.link-button:hover:not(:disabled) {
    background: var(--fill);
    color: var(--ink);
}

.link-button--danger:hover:not(:disabled) {
    background: var(--danger-fill);
    color: var(--danger);
}

/*
    The one action on a card that is the point of the card. Everything else in the row is
    housekeeping and stays quiet; this is filled, a size up, carries a pen, and stands a little
    apart from the rest so the row reads as one action and three afterthoughts.

    Pulled back onto the title's edge: .entry__actions is shifted left by the width of a quiet
    button's padding so *their* text lines up, and a filled button has an edge of its own to line up.
*/
.button--log {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.5rem 1.05rem 0.5rem 0.85rem;
    margin: 0 0.5rem 0 0.6rem;
    font-size: 0.95rem;
    font-weight: 600;
    background: var(--accent);
    color: var(--accent-ink);
    box-shadow: 0 1px 2px rgb(0 0 0 / 12%);
}

.button--log svg {
    width: 0.95em;
    height: 0.95em;
}

.button--log:hover:not(:disabled) {
    background: var(--accent-hover);
}

/* While the form is open the same button says Cancel, which is not an action to shout about. It
   keeps its size so the row does not jump, and drops to a tint so the form below it is the loud thing. */
.button--log--open {
    background: var(--accent-soft);
    color: var(--accent);
    box-shadow: none;
}

.button--log--open:hover:not(:disabled) {
    background: var(--fill-hover);
    color: var(--ink);
}

.chip {
    padding: 0.3rem 0.85rem;
    border-radius: 999px;
    font-size: 0.85rem;
    color: var(--ink-soft);
}

.chip--on {
    background: var(--accent);
    color: var(--accent-ink);
    font-weight: 600;
}

.chip--on:hover:not(:disabled) {
    background: var(--accent-hover);
}

.filters {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}

/* Boot -------------------------------------------------------------------- */

/*
    What is on screen while the WebAssembly runtime downloads — before there is an application to
    render anything. It is the first thing anyone sees on a cold visit, so it is centred and it
    says how far along it is, rather than sitting in the top-left corner saying "Loading…".
*/
.splash {
    /* One pace for the page turn and the falling books, so the two waits never drift apart. */
    --cycle: 2s;

    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1.25rem;
    padding: 2rem;
}

/*
    A book being read: two pale pages, a spine, and one leaf lifting off the right-hand page,
    turning over, and settling on the left. The leaf fades into the left page once it has landed
    rather than snapping back to start again, so the loop has no seam; the next leaf fades in on
    the right. Seen from above, on a short perspective, so the turn has some depth to it.
*/
.splash__book {
    position: relative;
    width: 3.8rem;
    height: 2.5rem;
    perspective: 14rem;
}

/* The spine: a hairline of page colour between the two halves. */
.splash__book::before {
    content: "";
    position: absolute;
    left: calc(50% - 1px);
    top: 0;
    bottom: 0;
    width: 2px;
    background: var(--page);
    z-index: 1;
}

.splash__page {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 50%;
    background: var(--accent);
    opacity: 0.3;
}

.splash__page--left { left: 0; border-radius: 3px 1px 1px 3px; }
.splash__page--right { right: 0; border-radius: 1px 3px 3px 1px; }

.splash__leaf {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 50%;
    border-radius: 1px 3px 3px 1px;
    background: var(--accent);
    transform-origin: left center;
    animation: turn var(--cycle) var(--ease) infinite;
}

@keyframes turn {
    0%        { transform: rotateY(0deg); opacity: 0; }
    10%       { transform: rotateY(0deg); opacity: 1; }
    55%       { transform: rotateY(-180deg); opacity: 1; }
    70%       { transform: rotateY(-180deg); opacity: 1; }
    88%, 100% { transform: rotateY(-180deg); opacity: 0; }
}

/*
    Three waits, one chosen at random ---------------------------------------

    The page turn above is no longer the only one. index.html draws all three and picks one on the
    first paint, writing the answer to the root element; every screen drawn afterwards — the boot
    screen and each Settling — reads that same answer, so a reader gets one wait per visit rather
    than a different one each time something loads. Picking per-component would break the thing
    Settling exists to protect: the handover from the runtime's screen to the app's is invisible
    only while both are showing the same shape.

    No attribute at all leaves the page turn showing. That is what a blocked script, a cached
    index.html from before this, or a reader with JavaScript off will get, and it is exactly what
    was here before — the random pick is an embellishment on a screen that already worked.
*/
.splash__stack,
.splash__reader {
    display: none;
}

:root[data-loader="stack"] .splash__book { display: none; }
:root[data-loader="stack"] .splash__stack { display: flex; }

:root[data-loader="reader"] .splash__book { display: none; }
:root[data-loader="reader"] .splash__reader { display: block; }

/*
    Books landing on a pile. Everything is in em against the one font-size below, so the whole
    thing is resized by changing that single value.

    Nothing between this and the screen edge may clip: a book begins its fall 2.6em above the pile,
    which is outside this box. On the splash there is nothing but empty page up there.
*/
.splash__stack {
    flex-direction: column-reverse;
    align-items: center;
    justify-content: flex-start;
    /* Air between the books. Tighter than this and five of them read as one
       hatched block rather than as five things that were put down separately. */
    gap: 0.2em;
    height: 2.75em;
    font-size: 1rem;
}

/*
    One green, the page turn's own, so the two are plainly the same app talking. Every book stays
    level — nothing rotates. With neither colour nor tilt telling them apart, that work is done by
    width and by where each comes to rest (--x), and no two agree: a pile squared up in the hand
    but not in the eye.

    The offsets alternate, and they are just big enough to be read as alternating. Too small and
    they average out into a pile that merely looks slightly off-centre; too big and the pile stops
    being a pile and becomes a zigzag, which is a pattern rather than an accident. This is the
    middle of that: the eye follows the lean from book to book — the thing that says somebody put
    these down one at a time — while the stack still reads as sitting on one spot. The two sides
    get different magnitudes as well, so the leans do not answer each other exactly.

    No curve on the shorthand; the drop sets its own below.
*/
/*
    The lean is `left` on a relatively positioned book, not a translateX inside the keyframes.
    Two reasons. It keeps the animated property to translateY and opacity alone, which is the pair
    a browser is happiest handing to the compositor — a transform whose value is built out of a
    custom property is the kind of thing that quietly stops qualifying. And it separates the two
    ideas: where a book sits is a static fact about that book, and the only thing the animation is
    for is the falling.
*/
.splash__stack-book {
    position: relative;
    height: 0.34em;
    border-radius: 0.08em;
    background: var(--accent);
    animation: stack var(--cycle) infinite;
}

/*
    The cycle is the page turn's own (--cycle, on .splash), so the two waits keep the same pace
    whichever one a visit draws. It was 1.6s, which read as hurried next to it: the books came
    down in a rush and were gone again. The stagger is 0.12s, in proportion. It is still drawn over waits that
    are frequently short — a book's details arrive in a few hundred milliseconds — so the pile
    is down by 0.96s rather than the 0.78s it was, but the first book is landing at 0.6s, which
    is soon enough that a short wait still shows books arriving and not only falling.
*/
.splash__stack-book:nth-child(1) { width: 2.4em; left: -0.26em; animation-delay: 0s; }
.splash__stack-book:nth-child(2) { width: 2.0em; left:  0.32em; animation-delay: 0.12s; }
.splash__stack-book:nth-child(3) { width: 2.6em; left: -0.34em; animation-delay: 0.24s; }
.splash__stack-book:nth-child(4) { width: 1.8em; left:  0.22em; animation-delay: 0.36s; }
.splash__stack-book:nth-child(5) { width: 2.2em; left: -0.3em;  animation-delay: 0.48s; }

/*
    Straight down, from two and a half book-heights of clear air, on a curve that accelerates: a
    thing that slows as it nears the ground is being lowered onto it, not dropped. No sideways
    drift either — each book falls plumb onto its own spot, which is what makes the off-centre
    landings read as a pile rather than as a slide-in.

    Smoother than it was in two ways. The fall starts more gently and gathers less speed, so the
    book meets the pile at a pace the eye can follow instead of arriving at a snap; and the
    settle is a shallower, longer rise and return, each leg on a curve that eases at both ends,
    so the book comes to rest rather than bounces. A hold, then a slow fade, so nothing pops.
*/
@keyframes stack {
    0%        { opacity: 0; transform: translateY(-2.6em);
                animation-timing-function: cubic-bezier(0.4, 0, 0.8, 0.5); }
    30%       { opacity: 1; transform: translateY(0);
                animation-timing-function: var(--ease-out); }
    38%       { transform: translateY(-0.07em);
                animation-timing-function: var(--ease); }
    48%       { transform: translateY(0); }
    78%       { opacity: 1; transform: translateY(0); }
    90%, 100% { opacity: 0; transform: translateY(0.3em); }
}

/*
    A page setting itself on an e-reader — the device this app already knows about, in DeviceTokens
    and the KOReader plugin. The boot screen's own three values, applied to a device instead of a
    book: the still part in accent held back to the same 0.3 the pages use, the moving part in
    accent at full like the leaf, and --page for the gap between them.
*/
.splash__reader {
    position: relative;
    width: 2.5em;
    height: 3.4em;
    /* The deeper chin an e-reader has. */
    padding: 0.22em 0.22em 0.5em;
    border-radius: 0.2em;
    font-size: 0.8rem;
}

/* The bezel is a layer of its own rather than the box's background, so holding it back to a tint
   does not drag the screen and the type back with it. */
.splash__reader::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    border-radius: inherit;
    background: var(--accent);
    opacity: 0.3;
}

.splash__screen {
    position: relative;
    z-index: 1;
    display: block;
    width: 100%;
    height: 100%;
    overflow: hidden;
    border-radius: 0.04em;
    background: var(--page);
}

.splash__line {
    position: absolute;
    left: 0.16em;
    height: 0.1em;
    border-radius: 0.05em;
    background: var(--accent);
    transform-origin: left center;
    /* Shortened with the stack, and for the same reason: the page has to finish setting itself
       inside the wait it is drawn over, or a reader only ever sees the first two lines. */
    animation: render 1.6s var(--ease-out) infinite;
}

/* Ragged down the right, the way set type is. */
.splash__line:nth-child(1) { top: 0.3em;  width: 1.5em;  animation-delay: 0s; }
.splash__line:nth-child(2) { top: 0.62em; width: 1.15em; animation-delay: 0.09s; }
.splash__line:nth-child(3) { top: 0.94em; width: 1.4em;  animation-delay: 0.18s; }
.splash__line:nth-child(4) { top: 1.26em; width: 0.95em; animation-delay: 0.27s; }
.splash__line:nth-child(5) { top: 1.58em; width: 1.3em;  animation-delay: 0.36s; }

@keyframes render {
    0%, 4%    { transform: scaleX(0); opacity: 0; }
    20%, 76%  { transform: scaleX(1); opacity: 1; }
    88%, 100% { transform: scaleX(1); opacity: 0; }
}

/*
    The same wait, once the application is running. Identical to the boot screen on purpose — see
    Components/Settling.razor — so the handover from the runtime's own screen to this one cannot be
    seen. It inherits everything from .splash; the only difference is that it is laid over a page
    that already exists, so it fills the window rather than the document.
*/
.splash--settling {
    position: absolute;
    inset: 0;
    /* Over the header, which is the only other thing with a z-index this high: a loading screen
       with a bar across its top is two screens, and the bar's Sign in button on a page in the
       middle of signing in is worse than that. */
    z-index: 25;
    min-height: 100vh;
    /*
        The body's own canvas, painted again, rather than a flat fill of var(--page). What this
        covers it covers completely, and a flat fill covers the mesh along with everything else.
        On the Book page that showed: the nearest positioned ancestor is the 40rem column, so
        the wait was a slab of bare page colour down the middle with the mesh still there either
        side of it. And the mesh is not idle during that wait — the jacket's colours are gliding
        into it (see the registered properties above), which is the only thing on screen that is
        about the book being waited for.

        Attached to the viewport, as on the body, so the four fields land in the same places
        whatever this happens to be sitting in and its edges do not show.

        KEEP IN STEP with the mesh on body.
    */
    background-color: var(--page);
    background-image:
        radial-gradient(56rem 40rem at 12% 6rem, oklch(from var(--wash-accent) l c calc(h + var(--mesh-hue)) / calc(alpha * var(--mesh-glow))), transparent 66%),
        radial-gradient(52rem 38rem at 88% 2rem, oklch(from var(--wash-warm) l c calc(h + var(--mesh-hue)) / calc(alpha * var(--mesh-glow))), transparent 66%),
        radial-gradient(56rem 40rem at 76% 96%, oklch(from var(--wash-cool) l c calc(h + var(--mesh-hue)) / calc(alpha * var(--mesh-glow))), transparent 66%),
        radial-gradient(48rem 34rem at 14% 100%, oklch(from var(--wash-accent) l c calc(h + var(--mesh-hue)) / calc(alpha * var(--mesh-glow))), transparent 66%);
    background-attachment: fixed;
}

/* The download is finished by the time this draws; the bar says so rather than restarting. */
.splash__fill--settled {
    width: 100%;
}

.splash__title {
    margin: 0;
    font-weight: 700;
    font-size: 1.1rem;
    letter-spacing: -0.02em;
}

.splash__track {
    width: min(15rem, 60vw);
    height: 4px;
    border-radius: 999px;
    background: var(--fill-hover);
    overflow: hidden;
}

/* The runtime publishes its progress as a percentage on the document; this is that number, drawn.
   The fallback keeps a sliver visible before the first update arrives. */
.splash__fill {
    height: 100%;
    width: var(--blazor-load-percentage, 5%);
    border-radius: 999px;
    background: var(--accent);
    transition: width 200ms var(--ease);
}

/* Welcome ----------------------------------------------------------------- */

/*
    Full width of the window for the deck's sake: its jackets fan out well past the reading column
    on a wide screen, and clipped at the column's edge the deck would look cut out of a photograph.
    The card inside keeps to the column, like everything else the app reads in.
*/
.hero {
    position: relative;
    overflow: hidden;
    width: 100vw;
    margin: 0 calc(50% - 50vw);
}

/*
    The way in, as a card: the shelf's own glass, edge and shadow, so the welcome is a shelf with
    one card on it. Centred inside, because the deck below is centred: text ranged left over a deck
    ranged centre reads as two things that happened to land on the same page.
*/
.hero__card {
    width: min(49.5rem, 100% - 2.5rem);
    margin: 0 auto;
    padding: clamp(1.6rem, 3vw, 2.4rem) 1.25rem clamp(1.5rem, 3vw, 2.2rem);
    text-align: center;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--card-shelf);
    box-shadow: var(--shadow);
}

/* Cover flow --------------------------------------------------------------- */

/*
    A deck of jackets seen at an angle: one held face-on at the front, the rest leaning away to
    either side and receding into the dark. The width of a jacket is the unit everything else is
    measured in, so the deck scales as one thing.
*/
/*
    Sized by the window's height as well as its width. A deck measured only across gets taller as
    the window gets wider, and on a short wide screen it pushes its own caption off the bottom.
*/
.flow {
    --cover-w: clamp(7rem, min(16vw, 23vh), 13rem);

    /* How much bigger a jacket gets under the cursor. Read again by --headroom, below. */
    --pickup: 1.09;

    /*
        Room above the deck, worked out from the pickup rather than guessed at.

        A slot is a jacket with its own reflection hanging under it, so the slot's middle — the
        point everything is scaled about — sits exactly on the waterline where the two meet. That
        is deliberate: it is what keeps every jacket standing on the same surface however far back
        it has receded. It also means a jacket under the cursor grows *entirely upwards*, by the
        pickup's share of its full height, and the stage's mask clips anything that does not fit.
        A jacket is one and a half times its own width tall, hence the 1.5; the 0.03 is air.
    */
    --headroom: calc(var(--cover-w) * (1.5 * (var(--pickup) - 1) + 0.03));

    /* The floor under the jackets, for their reflections, in jacket widths. */
    --floor: 0.72;

    position: relative;
    /* A breath between the card and the tops of the jackets, over and above the headroom the
       pickup needs: with only that, the deck sat on the card's shadow. */
    margin-top: clamp(1.25rem, 3vw, 2rem);
    padding-bottom: clamp(2.75rem, 5vw, 4.25rem);
}

.flow__stage {
    position: relative;
    /*
        The jacket is one and a half times its own width. The rest is room: enough above for a
        jacket to grow into under the cursor, and rather more below for the reflection. The mask
        below clips to this box — mask-clip is border-box and there is no value for "do not clip" —
        so anything that needs to be seen has to fit inside it.

        The room above is --headroom rather than a number of its own, so that the two places the
        stage has to be tall enough for cannot drift apart.
    */
    height: calc(var(--cover-w) * (1.5 + var(--floor)) + var(--headroom));
    perspective: 1400px;
    perspective-origin: 50% 38%;
    /*
        A sideways drag turns the deck and a vertical one scrolls the page. Without this the
        browser claims both, and the deck — which fills most of a phone screen — never sees a
        gesture at all. See the touch handlers in CoverFlow.razor.
    */
    touch-action: pan-y;
    /* The deck carries on past both edges of the window rather than stopping at them. */
    mask-image: linear-gradient(to right, transparent, #000 15%, #000 85%, transparent);
    -webkit-mask-image: linear-gradient(to right, transparent, #000 15%, #000 85%, transparent);
}

/*
    Every book is in the deck at all times and only its numbers change, so turning the deck is one
    transition rather than a set of arrivals and departures. The component works out where each one
    stands; this only decides what the numbers mean.

    Repeated for :active because the app's buttons shrink a little when pressed, and that rule sets
    `transform` outright — on this button it would throw away the whole deck geometry for as long
    as the mouse was down. The press is done with --lift instead, further down.
*/
.flow__slot,
.flow__slot:active:not(:disabled) {
    transform:
        translateX(calc(-50% + var(--x) * var(--cover-w)))
        translateZ(calc(var(--depth) * -1rem))
        rotateY(calc(var(--turn) * 1deg))
        scale(calc(var(--scale) * var(--lift, 1)));
}

.flow__slot {
    position: absolute;
    top: var(--headroom);
    left: 50%;
    width: var(--cover-w);
    /* A jacket, not a button: none of the chrome the rest of the app's buttons carry, and none of
       their centred label either — a drawn jacket sets its title ranged left. */
    padding: 0;
    border-radius: 0;
    background: none;
    text-align: left;
    /* Slivers on the far side are drawn at zero opacity and take no clicks. */
    pointer-events: none;
    opacity: var(--fade);
    z-index: var(--layer);
    /*
        Unhurried, but not slower than the hand that moves it: the deck turns every few seconds now
        and can be dragged, and a turn that outlasts the gesture that asked for it feels like lag
        rather than weight.
    */
    transition: transform 620ms var(--ease-out), opacity 620ms var(--ease-out);
}

.flow__slot--lit {
    pointer-events: auto;
}

/* Neither the fill nor the dimming the app's buttons get — the jacket is the whole of the button. */
.flow__slot:hover:not(:disabled),
.flow__slot:disabled {
    background: none;
}

.flow__slot:disabled {
    opacity: var(--fade);
}

/* Enough of a step forward to feel picked up, not enough to shove its neighbours. The stage
   keeps room above the deck for exactly this much, so the two are set from one number. */
.flow__slot--lit:hover {
    --lift: var(--pickup);
}

.flow__slot--lit:active {
    --lift: 1.04;
}

/*
    The shelf's jacket casts its shadow downwards, which here would fall in the one place the
    reflection needs to be — a dark band between a book and its own reflection, which reads as the
    reflection belonging to something else. On the stage the light is even and the shadow spreads
    rather than drops.
*/
.flow .cover {
    box-shadow: 0 0 1.5rem rgb(0 0 0 / 45%);
}

/*
    The fore edge — the stack of paper down the open side, opposite the spine. A jacket with
    nothing there reads as a printed rectangle standing on end; a few millimetres of page block is
    most of what tells the eye it is looking at a book.

    Always on the right, because every jacket in the deck faces us: the spine is on the left of a
    front cover whichever way the book is leaning. Sized from the jacket's own width so it stays in
    proportion on a phone. Above .cover__art, so a real cover does not paint over it.
*/
.flow .cover::after {
    content: "";
    position: absolute;
    top: 1.5%;
    right: 0;
    bottom: 1.5%;
    z-index: 1;
    width: calc(var(--cover-w) * 0.035);
    border-radius: 0 2px 2px 0;
    background:
        /* Single leaves, just visible — without them the block reads as a white stripe. */
        repeating-linear-gradient(to right, rgb(90 76 56 / 22%) 0 1px, transparent 1px 3px),
        linear-gradient(to right, rgb(0 0 0 / 30%), #efe9dd 30%, #fdfaf4 65%, rgb(120 104 78 / 45%));
}

/*
    Sat in the last of the stage rather than under it. The reflection has faded to nothing well
    before the stage ends, so this costs no height at all and puts the title where iTunes put it:
    in the dark just below the book, over the tail of its own reflection.
*/
.flow__caption {
    position: absolute;
    inset-inline: 0;
    bottom: clamp(1.25rem, 2.5vw, 2rem);
    margin: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.2rem;
    text-align: center;
    color: var(--ink);
}

/*
    Re-created whenever the book at the front changes — see the @key in CoverFlow.razor — so this
    plays again each turn. The deck takes a beat to bring the new jacket round; a caption that has
    already changed by then is labelling the wrong book for the length of the turn.
*/
.flow__caption {
    animation: rise 520ms var(--ease-out) both;
    animation-delay: 120ms;
}

.flow__caption strong {
    font-family: Georgia, "Times New Roman", serif;
    font-size: 1.15rem;
    font-weight: 600;
    letter-spacing: -0.01em;
}

.flow__caption span {
    font-size: 0.7rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

/*
    A jacket, drawn rather than photographed: pale board, a serif title, a rule, the author in
    small caps, and a darker strip down the spine edge. Set like a book because a coloured
    rectangle with a name on it reads as a missing image.
*/
.cover {
    position: relative;
    width: 100%;
    aspect-ratio: 2 / 3;
    /*
        The reflection is a second copy of this jacket, flipped — see .cover--mirror below and the
        markup in CoverFlow.razor. It used to be -webkit-box-reflect, which needs no second
        element but is a non-standard property Firefox has never implemented and mobile browsers
        drop often enough that the deck turned up on a phone standing on nothing at all.
    */
    border-radius: 3px;
    padding: 1rem 0.85rem;
    border-left: 4px solid rgb(0 0 0 / 16%);
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    overflow: hidden;
    box-shadow: 0 2px 4px rgb(0 0 0 / 25%), 0 12px 24px rgb(0 0 0 / 30%);
    text-wrap: balance;
}

.cover__title {
    font-family: Georgia, "Times New Roman", serif;
    font-size: 0.95rem;
    line-height: 1.15;
    letter-spacing: -0.01em;
}

.cover__rule {
    width: 1.6rem;
    height: 1px;
    margin: 0.55rem 0;
    background: currentColor;
    opacity: 0.45;
}

.cover__author {
    font-size: 0.6rem;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    opacity: 0.7;
}

/* The real jacket, over the drawn one. Nothing here hides the drawn jacket when this fails to
   load: an <img> with no image simply has nothing to paint, and what is behind it shows. */
.cover__art {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/*
    In the deck the jackets are held back until every one of them has arrived, and then brought up
    together — see CoverFlow.razor. Seventeen covers landing one by one in front of the reader is
    the deck assembling itself on screen; the same seventeen arriving at once is a picture. What
    shows meanwhile is the drawn jacket underneath, which is a book either way.
*/
.flow .cover__art {
    opacity: 0;
    transition: opacity 600ms var(--ease-out);
}

.flow--dressed .cover__art {
    opacity: 1;
}

/*
    The jacket as the floor sees it: the same drawn cover and the same photograph, flipped, fading
    out as it gets further from the book.

    The mask is read in the jacket's own coordinates and flipped along with it, so it runs
    bottom-up: the end of this gradient is the edge that touches the book. Written the obvious way
    round it fades to nothing exactly where the reflection should be strongest, and leaves a band
    of it floating below with a gap in between.

    The 1px gap is the thickness of the surface. Without it the book and its reflection meet
    exactly and the pair reads as one tall symmetrical object rather than a book on a floor.
*/
.cover--mirror {
    margin-top: 1px;
    transform: scaleY(-1);
    /* A reflection in a page, not in a black floor: fainter, or it reads as a second jacket. */
    opacity: 0.55;
    mask-image: linear-gradient(transparent 62%, rgb(0 0 0 / 26%) 84%, rgb(0 0 0 / 78%));
    -webkit-mask-image: linear-gradient(transparent 62%, rgb(0 0 0 / 26%) 84%, rgb(0 0 0 / 78%));
    /* A reflection is not a thing you can pick up. */
    pointer-events: none;
}

/*
    A reflection does not cast its own shadow: the drop shadow the jacket carries would appear
    under the reflection, which is to say floating in mid-air below the floor.
*/
.flow .cover--mirror,
.cover--mirror {
    box-shadow: none;
}

/* Six made-up jackets, so the shelf has the variety a real one does. */
.cover--0 { background: #e8dfd1; color: #3b3227; }
.cover--1 { background: #c2d3cc; color: #24352e; }
.cover--2 { background: #dcc0a6; color: #43301f; }
.cover--3 { background: #b8c6d7; color: #26303d; }
.cover--4 { background: #d9b8b1; color: #402724; }
.cover--5 { background: #cfc8a6; color: #38341f; }

/* The same small spaced capitals as the header's wordmark. */
.hero__greeting {
    margin: 0 0 0.6rem;
    font-size: 0.72rem;
    font-weight: 650;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.hero__title {
    font-size: clamp(2.6rem, 9vw, 4.25rem);
    line-height: 0.95;
    letter-spacing: -0.04em;
    font-weight: 800;
    margin: 0;
}

.hero__pitch {
    margin: 1rem auto 0;
    font-size: clamp(1.05rem, 2.6vw, 1.35rem);
    line-height: 1.35;
    max-width: 30ch;
    color: var(--ink-soft);
}

.hero__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 0.6rem;
    margin-top: 1.5rem;
}

/* The one filled action on the page, in the accent, the same shape as Log reading on a card. */
.hero__actions button {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.7rem 1.5rem;
    font-size: 1.02rem;
    font-weight: 600;
    border-radius: 12px;
    background: var(--accent);
    color: var(--accent-ink);
}

.hero__actions button:hover:not(:disabled) {
    background: var(--accent-hover);
}

.hero__actions button svg {
    width: 1rem;
    height: 1rem;
    flex: 0 0 auto;
}

.hero__actions input {
    padding: 0.7rem 0.9rem;
    border-radius: 12px;
}

.hero__actions [role="alert"] {
    flex-basis: 100%;
    font-size: 0.85rem;
    color: var(--ink-soft);
}

.pitch {
    max-width: 34rem;
    margin: 3rem auto;
    text-align: center;
    font-size: clamp(1.1rem, 3vw, 1.4rem);
    line-height: 1.45;
    letter-spacing: -0.015em;
    color: var(--ink-soft);
}

.saying {
    margin: 0;
}

.saying__text {
    margin: 0;
    font-style: italic;
    quotes: "\201C" "\201D";
}

/* Typographer's quotes round the words, hung so the text still aligns with what is above. */
.saying__text::before {
    content: open-quote;
    margin-left: -0.45em;
}

.saying__text::after {
    content: close-quote;
}

/* The saying itself is what is set large; the name is a footnote, upright and smaller. */
.saying__author {
    margin-top: 0.7rem;
    font-size: 0.7em;
    letter-spacing: 0.02em;
    color: var(--ink);
}

.saying__author cite {
    font-style: normal;
}

.saying__author::before {
    content: "— ";
}

/* Shelf ------------------------------------------------------------------- */

.shelf__heading {
    display: flex;
    flex-direction: column;
    gap: 0.85rem;
    margin-bottom: 1.25rem;
}

.entries {
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
}

/*
    Sheer, and with no backdrop filter of any kind. Behind a card there is nothing but the mesh,
    and a blurred soft gradient is the same soft gradient — measured at under 5/255 of difference
    anywhere on the shelf — while the cost of the filter is anything but nothing: any backdrop
    filter, blur or not, makes the browser re-sample what is under the card every time the card
    moves or the screen behind it changes. The cards move as they arrive, and on a phone the
    screen behind them changes on every frame of a scroll as the toolbar folds away. A shelf of
    cards each doing that was the catch in the scroll.
*/
.entry {
    display: flex;
    align-items: stretch;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--card-shelf);
    box-shadow: var(--shadow);
    transition:
        box-shadow 220ms var(--ease-out),
        border-color 220ms var(--ease-out),
        opacity 220ms var(--ease);
}

/*
    Cards arrive rather than appear, a beat apart, the way search results do; ShelfEntry hands its
    position down as --at. Filled backwards rather than both: a card that kept its final frame
    would keep its transform with it, and the hover lift below could never move it again.
*/
.entries > .entry {
    animation: rise 420ms var(--ease-out) backwards;
    animation-delay: calc(min(var(--at, 0), 8) * 45ms);
}

/*
    Answers the cursor with a deeper shadow and a firmer edge, and does not move. The card is a
    pane of glass — it blurs what is behind it — and a pane that slides even a pixel is re-drawn
    at fractional positions every frame of the slide: the text shimmers and the blur swims under
    it. The shadow lives outside the pane and can change as much as it likes.
*/
.entry:hover {
    border-color: var(--line-strong);
    box-shadow: var(--shadow-lifted);
}

/* Dimmed over a few frames, by the transition above: a card that fades is busy; one that blinks is broken. */
/*
    A card with a change in flight. The fade waits before it starts: a change that is answered
    promptly never shows it at all, and a card that dims and comes back inside a third of a second
    reads as a stutter rather than as work. Coming back is immediate -- .entry's own transition --
    so the answer lands the moment it arrives.
*/
.entry--working {
    opacity: 0.6;
    transition: opacity 160ms var(--ease) 260ms;
}

/*
    Not while a sheet is open from it. Anything below full opacity is a stacking context of its
    own, so a sheet inside a dimmed card is dimmed with it and its layers (22, 24) only count
    among the card's own: it would drop under the header and the cards after it for as long as
    the change took. The sheet has its scrim; the card behind it can wait undimmed.
*/
.entry--working:has(.sheet, .preview) {
    opacity: 1;
}

/*
    A book set aside — on hold, or dropped — in black and white. The jacket is desaturated and the
    accent is turned to grey for everything on the card that used it: the figure, the bar, the
    Log reading button. Done by re-pointing the tokens rather than filtering the whole card, so
    the type stays crisp ink and the card costs the browser nothing it did not already.
*/
.entry--parked {
    --accent: var(--ink-soft);
    --accent-hover: var(--ink);
    --accent-soft: var(--fill);
    --accent-ink: var(--card);
}

.entry--parked .entry__cover {
    filter: grayscale(1);
    opacity: 0.8;
}

/*
    Read to the end: the card's accent turns to the amber and the cover goes warm and a little
    faded, like a jacket that has been on the shelf a while. Colour is a book alive, grey one set
    aside, warm one done — the same trick as parked, so a reader who has learnt one has learnt
    the other. The figure is a word rather than a number, so it is set a size down from one.
*/
.entry--finished {
    --accent: var(--warn);
    --accent-hover: var(--warn-hover);
    --accent-soft: var(--warn-soft);
}

.entry--finished .entry__cover {
    filter: sepia(0.75) saturate(0.7) contrast(0.92);
}

.entry--finished .progress__figure strong {
    font-size: 1.15rem;
    letter-spacing: -0.01em;
}

/*
    The shelf before Library has answered — see Home.razor. Each ghost is an .entry with bones
    where the cover, title, authors and status will be, so nothing moves when the real cards take
    their places: same padding, same gap, near enough the same height.

    Held back for a beat before showing. On a warm shelf the answer is back well inside that, and
    three ghosts that flash up and are replaced would be a stutter the wait itself never had. Past
    the beat they rise in one after another and shimmer until the answer comes.
*/
.entries--pending {
    pointer-events: none;
}

.entries > .entry--pending {
    animation-duration: 360ms;
    animation-delay: calc(140ms + var(--at, 0) * 70ms);
}

.bone {
    border-radius: 6px;
    background: linear-gradient(100deg, var(--fill) 30%, var(--fill-hover) 50%, var(--fill) 70%);
    background-size: 250% 100%;
    animation: shimmer 1.8s linear infinite;
}

.bone--cover {
    aspect-ratio: 2 / 3;
    border-radius: 8px;
}

/* The bones' boxes, margins included, add up to the rows they stand in for — title, authors, the
   status control, the row of actions — so the ghost is the height of the card to the pixel. All the
   spacing is on the underside: split across both sides it would collapse between neighbours. */
.bone--title {
    height: 1rem;
    width: 58%;
    margin: 0.2rem 0 0.65rem;
}

.bone--authors {
    height: 0.75rem;
    width: 34%;
    margin: 0 0 1rem;
}

.bone--status {
    height: 2rem;
    width: 8.5rem;
    margin: 0 0 1.1rem;
    border-radius: var(--radius-sm);
}

.bone--actions {
    height: 0.7rem;
    width: 15rem;
    max-width: 100%;
    margin: 0 0 0.6rem;
}

/* Three identical ghosts read as a pattern; three that differ the way titles do read as books. */
.entry--pending:nth-child(2) .bone--title { width: 44%; }
.entry--pending:nth-child(2) .bone--authors { width: 28%; }
.entry--pending:nth-child(3) .bone--title { width: 66%; }
.entry--pending:nth-child(3) .bone--authors { width: 40%; }

/* The highlight crosses left to right, which is the direction the eye is already reading in. */
@keyframes shimmer {
    from { background-position: 100% 0; }
    to { background-position: 0% 0; }
}

.entry__cover,
.result img,
.result__cover-placeholder {
    flex: 0 0 auto;
    width: 3.25rem;
    border-radius: 8px;
    overflow: hidden;
}

.entry__cover img {
    display: block;
    width: 100%;
    border-radius: 8px;
}

.entry__cover-placeholder,
.result__cover-placeholder {
    display: grid;
    place-items: center;
    aspect-ratio: 2 / 3;
    background: var(--accent-soft);
    color: var(--accent);
    font-weight: 700;
    font-size: 1.2rem;
    border-radius: 8px;
}

/*
    On the card the cover is not a thumbnail but a strip down its left edge, the full height of
    the card, cropped to fit: however tall the card grows, the jacket grows with it, so there is
    never a column of empty card under a small cover. The body beside it carries the card's
    padding. A phone lays the card out another way — see the phone rules.
*/
.entry__cover {
    position: relative;
    width: 8.25rem;
    min-height: 11rem;
    border-radius: calc(var(--radius) - 1px) 0 0 calc(var(--radius) - 1px);
}

.entry__cover > * {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 0;
    aspect-ratio: auto;
}

/*
    Title and authors top left, the status top right, then the progress on a line of its own —
    the figure at its left end, what it counts at its right — the bar across beneath, and
    everything after that full width — see ShelfEntry.razor. minmax(0, 1fr) rather than 1fr so
    a long title wraps instead of pushing the status off the card.
*/
.entry__body {
    flex: 1 1 auto;
    min-width: 0;
    padding: 1.1rem 1.3rem;
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: 1rem;
    row-gap: 0.55rem;
    align-items: start;
}

.entry__body > * {
    grid-column: 1 / -1;
}

.entry__head {
    grid-column: 1;
    grid-row: 1;
    min-width: 0;
}

.entry__title {
    line-height: 1.3;
    font-size: 1.05rem;
}

.entry__authors {
    margin: 0.15rem 0 0;
    color: var(--ink-soft);
    font-size: 0.9rem;
}

/*
    A book wanted and not begun has no figure or bar — see ShelfEntry.razor — so what is known of
    it takes their place under the authors: its length and when it was added, in the ink the
    authors are in, a size down. The cover strip is no longer held up by a progress block, so it
    comes down with the card.
*/
.entry__meta {
    margin: 0.4rem 0 0;
    color: var(--ink-soft);
    font-size: 0.85rem;
    font-variant-numeric: tabular-nums;
}

.entry--unstarted .entry__cover {
    min-height: 9.5rem;
}

/* Top right of the card, level with the title: where a book stands is the one control worth
   reading before the reading itself, and up here it is out of the row of actions. */
.entry__status {
    grid-column: 2;
    grid-row: 1;
    justify-self: end;
    align-self: start;
    display: flex;
    align-items: center;
    gap: 0.85rem;
    flex-wrap: wrap;
}

/*
    The status as a flat control — a fill, a dot in the standing's colour, the word, a chevron —
    and a list of its own beneath, drawn here rather than by the platform. Built the way the
    account menu is: trigger, scrim, menu; and the same frosted glass, in the @supports block
    with the header's.

    The dot is the standing's colour, not the card's accent: a book wanted is on a green card
    too, and its dot has to say "not yet" on its own — so it is a ring, and dropped is a ring in
    grey against on hold's solid grey.
*/
.status {
    position: relative;
}

.status__trigger {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.3rem 0.5rem 0.3rem 0.6rem;
    background: var(--fill);
    color: var(--ink);
    font-size: 0.85rem;
    font-weight: 600;
    white-space: nowrap;
}

.status__trigger:hover:not(:disabled),
.status__trigger[aria-expanded="true"] {
    background: var(--fill-hover);
}

.status__chevron {
    width: 0.85em;
    height: 0.85em;
    color: var(--ink-faint);
    transition: transform var(--quick) var(--ease);
}

.status__trigger[aria-expanded="true"] .status__chevron {
    transform: rotate(180deg);
}

.status__dot {
    flex: 0 0 auto;
    width: 0.55rem;
    height: 0.55rem;
    border-radius: 50%;
    background: var(--dot, var(--ink-faint));
}

/*
    The trigger's dot takes the card's accent, which already agrees with the standing — green
    (or the jacket's colour, on the Book page) for a book in hand, grey parked, amber finished.
    A list's dots cannot: on a parked card the accent is grey, and the reader choosing there
    still needs to see which rows are the live ones. So lists carry the statuses' own colours.
*/

.status__trigger--wanttoread,
.status__trigger--reading { --dot: var(--accent); }
.status__trigger--finished { --dot: var(--warn); }
.status__trigger--onhold,
.status__trigger--dropped { --dot: var(--ink-faint); }

.status__row--wanttoread,
.status__row--reading,
.preview__status--wanttoread,
.preview__status--reading,
.import__tone--wanttoread,
.import__tone--reading { --dot: var(--status-alive); }
.status__row--finished,
.preview__status--finished,
.import__tone--finished { --dot: var(--status-done); }
.status__row--onhold,
.status__row--dropped,
.import__tone--onhold,
.import__tone--dropped { --dot: var(--ink-faint); }

/* Not yet begun, and given up: rings rather than dots. */
.status__trigger--wanttoread .status__dot,
.status__row--wanttoread .status__dot,
.preview__status--wanttoread .status__dot,
.import__tone--wanttoread .status__dot,
.status__trigger--dropped .status__dot,
.status__row--dropped .status__dot,
.import__tone--dropped .status__dot {
    background: transparent;
    box-shadow: inset 0 0 0 1.5px var(--dot);
}

.status__scrim {
    position: fixed;
    inset: 0;
    z-index: 1;
    padding: 0;
    border-radius: 0;
    background: transparent;
    cursor: default;
}

.status__scrim:hover:not(:disabled) {
    background: transparent;
}

.status__menu {
    position: absolute;
    top: calc(100% + 0.35rem);
    right: 0;
    z-index: 2;
    min-width: 11.5rem;
    margin: 0;
    padding: 0.35rem;
    list-style: none;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--card);
    box-shadow: var(--shadow-lifted);
    animation: unfold 240ms var(--ease-out) backwards;
}

.status__row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    margin: 0;
    padding: 0.5rem 0.6rem;
    border-radius: 8px;
    background: transparent;
    color: var(--ink);
    font-size: 0.92rem;
    font-weight: 500;
    text-align: left;
    white-space: nowrap;
    transition: background-color var(--quick) var(--ease);
}

.status__row:hover:not(:disabled),
.status__row:focus-visible {
    background: var(--fill);
}

/* The row the book is on, tinted in its own dot's colour. */
.status__row--on,
.status__row--on:hover:not(:disabled),
.status__row--on:focus-visible {
    background: color-mix(in srgb, var(--dot) 13%, transparent);
    color: var(--dot);
    font-weight: 600;
}

.status__tick {
    width: 0.9em;
    height: 0.9em;
    margin-left: auto;
}

/* Pulled out to the card's edge, so a row of quiet actions lines up with the title above it. */
.entry__actions {
    display: flex;
    gap: 0.2rem;
    flex-wrap: wrap;
    margin: 0.1rem 0 0 -0.6rem;
    font-size: 0.85rem;
}

/*
    On the card the log is a word in the accent among quiet words, not a filled button: the
    card's loud thing is the figure, and a fill under it made the foot of the card heavier than
    its head. It keeps the pen, and its own colour, so it still reads as the one that does
    something. On a phone it is the big button again — see the phone rules.
*/
.entry__actions .button--log {
    margin: 0;
    padding: 0.3rem 0.6rem;
    font-size: 0.9rem;
    background: none;
    box-shadow: none;
    color: var(--accent);
}

.entry__actions .button--log:hover:not(:disabled) {
    background: var(--accent-soft);
}

.entry__actions .button--log--open {
    background: var(--accent-soft);
}

.entry__actions .link-button {
    font-size: 0.9rem;
}

.entry__reasons {
    margin: 0.75rem 0 0;
    padding: 0.6rem 0.85rem;
    list-style: none;
    background: var(--danger-soft);
    color: var(--danger);
    border-radius: var(--radius-sm);
    font-size: 0.9rem;
}

.entry__reasons li + li {
    margin-top: 0.25rem;
}

/*
    Settings: three settings side by side where the card is wide enough, each centred over its
    own hint — a small label, the control, a line on what it does. The controls are the app's,
    not the platform's: Track by is the status's trigger and list (TrackingSelect), and the
    edition is found with the main search's field and cards.
*/
.settings {
    text-align: center;
}

.settings__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: 1.25rem 1.5rem;
    padding: 0.35rem 0 0.15rem;
}

.settings__item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    min-width: 0;
}

/* The small capitals of the shelf's group headings. */
.settings__label {
    font-size: 0.72rem;
    font-weight: 650;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.settings__item .hint {
    max-width: 16rem;
    margin: 0;
    text-wrap: balance;
}

.settings__item .hint .link-button {
    display: inline;
    padding: 0;
    font-size: inherit;
}

.settings__pages {
    display: flex;
    gap: 0.4rem;
}

.settings__pages input {
    width: 5.5rem;
    text-align: center;
}

/* No standing to colour it, so the list's tint is the card's. */
.tracking {
    --dot: var(--accent);
}

/* Under its trigger, centred on it, whatever the width of the column it is in. */
.settings .status__menu {
    right: auto;
    left: 50%;
    width: 11.5rem;
    min-width: 0;
    margin-left: -5.75rem;
    text-align: left;
}

.settings__find svg {
    width: 0.95em;
    height: 0.95em;
    color: var(--ink-faint);
}

/* A phone's way out of a sheet; in the card the button that opened it closes it. */
.sheet__foot {
    display: none;
}

/* The edition search is dragged by this only where it is a sheet, on a phone (see there). */
.settings .search__grip {
    display: none;
}

.settings__back {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    float: left;
}

.settings__back svg {
    width: 0.95em;
    height: 0.95em;
}

.settings__search {
    clear: both;
    display: flex;
    gap: 0.5rem;
    max-width: 34rem;
    margin: 0 auto;
    padding-top: 0.5rem;
}

.settings__search .button--primary {
    flex: 0 0 auto;
    padding-inline: 1.25rem;
    font-weight: 600;
}

.settings__note {
    margin: 0.45rem 0 0;
}

/*
    The results scroll in a window of their own, so a page of editions is read inside the panel
    rather than by scrolling the page down to the foot of it.
*/
.settings__results {
    max-height: 24rem;
    margin-top: 0.9rem;
    padding: 0.1rem 0.25rem 0.25rem;
    overflow-y: auto;
    overscroll-behavior: contain;
    text-align: left;
}

.settings__results:empty {
    display: none;
}

.settings__results .lib-grid {
    grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
}

.settings__none {
    margin: 0.5rem 0;
    text-align: center;
}

.settings__more {
    margin-top: 0.6rem;
    text-align: center;
}

/* Progress ---------------------------------------------------------------- */

/* Not a box of its own: its two pieces take their places in the card's grid. */
.progress {
    display: contents;
}

/*
    The headline number, on a line of its own under the title, with what it is a number of at
    the far end of the same line, as a caption. Accent, because it is the one fact on the card
    that changes as the reader reads. Full width, so on the card it falls under the title and
    the status both and the bar beneath it runs the same span.
*/
.progress__figure {
    grid-column: 1 / -1;
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
    line-height: 1.05;
}

.progress__figure strong {
    margin-right: auto;
}

.progress__figure strong {
    font-size: 1.6rem;
    font-weight: 700;
    letter-spacing: -0.03em;
    font-variant-numeric: tabular-nums;
    color: var(--accent);
}

/* Nothing to report yet is not a number, and is not coloured like one. */
.progress__figure--none strong {
    color: var(--ink-faint);
}

.progress__figure span {
    font-size: 0.85rem;
    line-height: 1.25;
    text-align: right;
    color: var(--ink-soft);
}

/* When a finished book was finished: the caption at the far end of the figure's line, in the
   voice of the caption a book in progress has there. */
.progress__when {
    font-size: 0.85rem;
    font-variant-numeric: tabular-nums;
    color: var(--ink-soft);
}

/* Full width, under the title and the figure both. Named explicitly: the wrapper is display:
   contents, so the card's own "everything else spans" rule does not reach this far down. */
.progress__track {
    grid-column: 1 / -1;
    height: 6px;
    background: var(--fill-hover);
    border-radius: 999px;
    overflow: hidden;
}

/* Grows to the new mark rather than snapping to it: logging a session is the one moment the bar
   is being watched, and a bar that visibly gains ground is the reward for having read. */
.progress__fill {
    height: 100%;
    background: var(--accent);
    border-radius: 999px;
    transition: width 560ms var(--ease-out);
}

.progress__track {
    position: relative;
}

/*
    Where the device is, as a tick on the track. Usually it sits at the end of the fill; it
    sits behind it when the reader flipped back, and ahead of it when the tracker has reading
    the device reported but the page count makes a smaller percentage of.
*/
.progress__bookmark {
    position: absolute;
    top: -2px;
    bottom: -2px;
    width: 2px;
    margin-left: -1px;
    background: var(--ink);
    border-radius: 1px;
}

.progress__device {
    grid-column: 1 / -1;
    margin: -0.15rem 0 0;
    font-size: 0.78rem;
    color: var(--ink-soft);
}

.progress__device strong {
    font-weight: 600;
    color: var(--ink);
}

.progress__device-when {
    color: var(--ink-faint);
}

.progress__device-when::before {
    content: "· ";
}

/* Panels ------------------------------------------------------------------ */

.panel {
    margin-top: 0.85rem;
    padding: 1rem;
    border-radius: var(--radius-sm);
    background: var(--fill);
}

/* A sheet's parts — the log form's and Settings': drawn only on a phone, where the panel comes up
   from the foot of the screen (see the max-width: 560px rules). In the card, as here, it is a
   panel like the others. */
.sheet__scrim,
.sheet__title {
    display: none;
}

.panel--danger {
    background: var(--danger-soft);
}

.panel > p:first-child {
    margin-top: 0;
}

/*
    Panels open rather than appear. They are conditional in the markup, so opening one builds new
    elements and this plays each time: a short drop from just above where it lands, as a thing
    unfolding from the row of actions that asked for it. Quicker than an arrival from below — a
    panel is answering a click, and the hand that clicked is waiting.
*/
@keyframes unfold {
    from { opacity: 0; transform: translateY(-0.3rem); }
    to { opacity: 1; transform: none; }
}

.panel,
.history-sheet,
.entry__reasons {
    animation: unfold 240ms var(--ease-out) backwards;
}

/* The phone's search dock, coming up from the foot of the screen it is docked to. */
@keyframes dock {
    from { opacity: 0; transform: translateY(1.5rem); }
    to { opacity: 1; transform: none; }
}

/*
    Leaving. Blazor takes a closed menu, sheet or panel out of the page at once; js/motion.js puts
    it back with .is-leaving for as long as one of these takes, so everything goes the way it
    came. A panel in a card fades and folds its height away, so the card closes up with it
    rather than snapping shut after it (where a browser cannot animate a height to nothing, it
    snaps its height halfway through instead). A history's unfolded days fold away the same way.
    A menu lifts back towards its trigger. On a phone, sheets drop back off the foot of the
    screen (the max-width: 560px rules).
*/
@keyframes fold {
    to {
        opacity: 0;
        height: 0;
        margin-block: 0;
        padding-block: 0;
    }
}

@keyframes lift {
    to { opacity: 0; transform: translateY(-0.3rem); }
}

@keyframes clear {
    to { opacity: 0; }
}

@keyframes undock {
    to { transform: translateY(100%); }
}

/* Coming, the same way backwards: a panel, or a day unfolded, grows its height open (js/motion.js). */
@keyframes grow {
    from {
        opacity: 0;
        height: 0;
        margin-block: 0;
        padding-block: 0;
    }
}

.panel.is-entering,
.sheet.is-entering,
.day--more.is-entering {
    interpolate-size: allow-keywords;
    overflow: hidden;
    animation: grow 240ms var(--ease-out) backwards;
}

.is-leaving {
    pointer-events: none;
}

.panel.is-leaving,
.sheet.is-leaving,
.day--more.is-leaving {
    interpolate-size: allow-keywords;
    overflow: hidden;
    animation: fold 200ms var(--ease) forwards;
}

.status__menu.is-leaving,
.account__menu.is-leaving,
.goal__menu.is-leaving {
    animation: lift 160ms var(--ease) forwards;
}

.status__scrim.is-leaving,
.account__scrim.is-leaving {
    animation: clear 160ms var(--ease) forwards;
}

/*
    A history's days past its fold, at a panel's timing (above) but with their own keyframes: the
    list's gap and each card's border go with the height, so many days folding together close up
    as they go rather than leaving their gaps to snap shut after them.
*/
@keyframes day-fold {
    to {
        opacity: 0;
        height: 0;
        margin-top: calc(-1 * var(--days-gap));
        border-width: 0;
    }
}

@keyframes day-grow {
    from {
        opacity: 0;
        height: 0;
        margin-top: calc(-1 * var(--days-gap));
        border-width: 0;
    }
}

.day--more.is-entering {
    animation-name: day-grow;
}

.day--more.is-leaving {
    animation-name: day-fold;
}

.field {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-bottom: 0.7rem;
}

.field:last-child {
    margin-bottom: 0;
}

.field label {
    min-width: 6.5rem;
    font-size: 0.9rem;
    color: var(--ink-soft);
}

.field input[type="number"] {
    width: 7rem;
}

.unit {
    font-size: 0.85rem;
    color: var(--ink-faint);
}

.field__note {
    color: var(--ink-faint);
    font-weight: 400;
}

.hint {
    flex-basis: 100%;
    margin: 0.25rem 0 0;
    font-size: 0.8rem;
    color: var(--ink-faint);
}

/* A hint that is telling the reader they got something wrong, rather than helping them along. */
.hint--bad {
    color: var(--danger);
}

.session-form {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1.25rem;
}

.session-form .field {
    margin-bottom: 0;
}

.session-form .field label {
    min-width: auto;
}

.session-form__actions {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    margin-top: 0.9rem;
}

/*
    Where the reader stopped (StopSlider): a head saying how much that is, the page itself between
    a nudge either side, and the book as one slider under them -- the part already read drawn in
    quiet ink, what this sitting adds in the accent, the rest bare.

    The browser keeps the thumb's centre half a thumb in from either end of the track, so a stop
    at a fraction f of the book sits at r + (width - 2r) * f, not at f of the width. The two parts
    are painted by that same sum, so the read part ends where the thumb starts from, to the pixel.
*/
.stop {
    --thumb: 1.75rem;
    --at-read: calc(var(--thumb) / 2 + (100% - var(--thumb)) * var(--read));
    --at-stop: calc(var(--thumb) / 2 + (100% - var(--thumb)) * var(--to));
    --track: linear-gradient(90deg,
        var(--ink-faint) 0 var(--at-read),
        var(--accent) var(--at-read) var(--at-stop),
        var(--fill-hover) var(--at-stop) 100%);

    flex: 1 1 100%;
    min-width: 0;
}

/* A book not yet begun has no read part, not even the half thumb before it: the green runs from zero. */
.stop--unread {
    --at-read: 0px;
}

.stop__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
}

.stop__label {
    font-size: 0.68rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.stop__gain {
    font-size: 0.85rem;
    font-weight: 650;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
    transition: opacity var(--quick) var(--ease);
}

/* Nothing added yet: the figure stays in its place, so the head does not shift when it arrives. */
.stop__gain--none {
    opacity: 0;
}

.stop__reading {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin: 0.35rem 0 0.2rem;
}

.stop__place {
    font-size: 1.35rem;
    font-weight: 700;
    line-height: 1.2;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.stop__of {
    margin-left: 0.2rem;
    font-size: 0.9rem;
    font-weight: 500;
    color: var(--ink-soft);
}

/* A thumb's worth of target, round, quiet until pointed at: the slider is the main control. */
.stop__nudge {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 2.5rem;
    height: 2.5rem;
    padding: 0;
    border-radius: 999px;
    font-size: 1.2rem;
    line-height: 1;
}

/* The input is as tall as a thumb can hit; the track drawn inside it is not. */
.stop__range {
    display: block;
    width: 100%;
    height: 2.75rem;
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    appearance: none;
    cursor: pointer;
    touch-action: pan-y;
}

.stop__range::-webkit-slider-runnable-track {
    height: 0.6rem;
    border-radius: 999px;
    background: var(--track);
}

.stop__range::-moz-range-track {
    height: 0.6rem;
    border-radius: 999px;
    background: var(--track);
}

.stop__range::-webkit-slider-thumb {
    appearance: none;
    width: var(--thumb);
    height: var(--thumb);
    margin-top: calc((0.6rem - var(--thumb)) / 2);
    border: 3px solid var(--accent);
    border-radius: 999px;
    background: var(--card);
    box-shadow: var(--shadow);
    transition: transform var(--quick) var(--ease);
}

.stop__range::-moz-range-thumb {
    box-sizing: border-box;
    width: var(--thumb);
    height: var(--thumb);
    border: 3px solid var(--accent);
    border-radius: 999px;
    background: var(--card);
    box-shadow: var(--shadow);
}

.stop__range:active::-webkit-slider-thumb {
    transform: scale(1.12);
}

.stop__range:focus-visible {
    outline: none;
}

.stop__range:focus-visible::-webkit-slider-thumb {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
}

.stop__range:focus-visible::-moz-range-thumb {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
}

/* The card's form ends in where the rest of it is: the Book page. */
.session-form__brief {
    margin: 0.4rem 0 0;
}

/* Sessions ---------------------------------------------------------------- */

.sessions {
    list-style: none;
    margin: 0;
    padding: 0;
}

.session {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
    padding: 0.35rem 0;
    font-size: 0.9rem;
    border-bottom: 1px solid var(--line);
}

.session:last-child {
    border-bottom: none;
}

.session__what {
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

.session__when,
.session__how-long {
    color: var(--ink-soft);
    font-size: 0.85rem;
}

.session__how-long::before {
    content: "· ";
}

/*
    Days. The history is one card per calendar day on the shelf card's own surface: the day
    named in a tinted row, what it came to on the right, and Correct and Delete for the day. The
    logs themselves — an e-reader's every few minutes — are not shown; a day is a number.
*/
.days {
    --days-gap: 0.5rem;

    display: flex;
    flex-direction: column;
    gap: var(--days-gap);
    margin: 0;
    padding: 0;
    list-style: none;
}

/*
    Never shrunk to fit: a day clips what overflows it, which lets a flex item go below its
    content's height, so in the capped box (.days--scrolling) every day was squeezed to fit
    rather than the box scrolling. Its height changes only as it folds or grows (day-fold, day-grow).
*/
.day {
    flex-shrink: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--card-shelf);
    box-shadow: var(--shadow);
    overflow: hidden;
}

.day__head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.2rem 0.6rem;
    padding: 0.5rem 0.85rem;
    background: var(--fill);
    font-size: 0.9rem;
}

.day__name {
    font-weight: 650;
}

.day__date {
    font-size: 0.78rem;
    color: var(--ink-faint);
}

.day__total {
    margin-left: auto;
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}

.day__ops {
    display: flex;
    gap: 0.1rem;
    margin-right: -0.6rem;
    font-size: 0.82rem;
}

/* The day restated: its name over the form, on the card's own surface rather than the tint. */
.day__edit {
    padding: 0.6rem 0.85rem 0.75rem;
}

.day__editing {
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
    margin: 0 0 0.4rem;
    font-size: 0.9rem;
}

/* Show more / Show less: the Book page's "Read more" pill (.book__more), under the days. */
.days__all {
    margin-top: 0.6rem;
}

/*
    A history unfolded: every day, in a box of its own that scrolls, so a long one does not push
    what is under it away. No taller than half the screen, so with a phone's keyboard up there is
    still page to take hold of, and a swipe that reaches the box's end goes on to the page. The
    day cards lie flat in it, since a box that scrolls cuts off any shadow at its edges; the
    padding, pulled back by the same amount, is room for a focus ring.
*/
.days--scrolling,
.days:has(> .day--more.is-leaving) {
    max-height: min(22rem, 50dvh);
    margin: -0.25rem;
    padding: 0.25rem;
    overflow-y: auto;
}

.days--scrolling .day,
.days:has(> .day--more.is-leaving) .day {
    box-shadow: none;
}

/* On the page: room above, and the count beside the heading in the eyebrow's quieter voice. */
.history {
    margin-top: 1.1rem;
}

.history__eyebrow {
    margin: 0 0 0.6rem;
}

.history__eyebrow span {
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
    color: var(--ink-soft);
}

/* A session the reader did not type: a small tag rather than a sentence. */
.session__source {
    padding: 0.05rem 0.4rem;
    border-radius: 999px;
    background: var(--accent-soft);
    color: var(--accent);
    font-size: 0.68rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

/* Search ------------------------------------------------------------------ */

/*
    On a wide screen the search is two quiet things: a field in a row under the tabs (.search-row),
    and — below the shelf — whatever the library found (.search). Neither is a card; a search is a
    different job from reading the shelf, but it does not need a surface of its own to say so. On a
    phone the whole lot becomes a bottom sheet; see the media query below.
*/
.search {
    margin-top: 0.5rem;
}

/* Where a page turn brings the page back to: the results' top, clear of the sticky header. */
.search__found {
    scroll-margin-top: var(--header-offset);
}

/* The drag handle and the dock's own field belong to the phone sheet; a wide screen shows neither. */
.search__grip {
    display: none;
}

.search__inputs {
    display: none;
    gap: 0.5rem;
}

/* The field under the tabs, on a wide screen. */
.search-row {
    display: flex;
    gap: 0.5rem;
    margin: 1.25rem 0 1.15rem;
}

.search-row .button--primary {
    flex: 0 0 auto;
    padding-inline: 1.25rem;
    font-weight: 600;
}

/* A group's heading — "On your shelf", "In the library" — with its count in a lighter voice. */
.group {
    margin: 1.5rem 0 0.7rem;
    font-size: 0.72rem;
    font-weight: 650;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.group:first-child {
    margin-top: 0;
}

.group span {
    font-weight: 500;
    letter-spacing: 0;
    text-transform: none;
    font-size: 0.85rem;
    color: var(--ink-soft);
}

/*
    The status headings down the whole shelf. The cards keep their gap from the column; the
    heading only needs a little more air above it than a card would, so each group reads as one.
*/
.entries > .group--shelf {
    margin: 0.9rem 0 0;
}

.entries > .group--shelf:first-child {
    margin-top: 0;
}

/* The line under the field while searching: what the shelf holds, and where the library stands. */
/* Tucked under the field it belongs to, rather than adding a line of its own to the gap below. */
.hint-line {
    margin: -0.7rem 0.2rem 1.15rem;
    font-size: 0.88rem;
    color: var(--ink-soft);
}

/* Out of the search's way, and out of reach: the search spans every status. */
.filters--muted {
    opacity: 0.4;
}

/*
    The library's answer: compact cards, two to a row, so a page of ten is one glance rather than
    a long scroll. Jacket, then title over author over page count, then the way onto the shelf —
    or, for a book already there, a quiet note that it is.
*/
.lib-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.6rem;
}

.lib-card {
    display: flex;
    align-items: center;
    gap: 0.8rem;
    min-width: 0;
    padding: 0.7rem 0.8rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--card-shelf);
    animation: rise 420ms var(--ease-out) both;
    animation-delay: calc(min(var(--at, 0), 8) * 45ms);
}

.lib-card img,
.lib-card .result__cover-placeholder {
    width: 2.4rem;
    flex: 0 0 auto;
    border-radius: 6px;
}

.lib-card__body {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.05rem;
}

.lib-card__body strong {
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.lib-card__body span {
    font-size: 0.85rem;
    color: var(--ink-soft);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.lib-card__pages {
    font-size: 0.78rem !important;
    color: var(--ink-faint) !important;
}

/* The jacket and the words, as one way into the book: the card's own layout, with none of a
   button's fill. Pointed at, the title takes the accent, as a link would. */
.lib-card__open,
.lib-card__open:hover:not(:disabled) {
    display: flex;
    flex: 1 1 auto;
    align-items: center;
    gap: 0.8rem;
    min-width: 0;
    padding: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: left;
}

.lib-card__open:active:not(:disabled) {
    transform: none;
}

.lib-card__open:hover strong {
    color: var(--accent);
}

/*
    A search result, opened (Components/BookPreview.razor): a sheet over the results, from the
    right on a wide screen and up from the foot on a phone. Over the header (20) like the other
    sheets, so nothing behind it answers but the scrim, which closes it. The body scrolls between
    a fixed close button and a fixed foot, so Add to shelf is always in reach however long the
    description runs.
*/
.preview__scrim,
.preview__scrim:hover:not(:disabled) {
    position: fixed;
    inset: 0;
    z-index: 21;
    padding: 0;
    border-radius: 0;
    background: rgb(0 0 0 / 40%);
    cursor: default;
    animation: fade 200ms var(--ease-out) backwards;
}

.preview {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 22;
    width: min(31rem, 100%);
    display: flex;
    flex-direction: column;
    /* The tint where there is no backdrop filter; glass where there is, as every other sheet. */
    background-color: var(--card-raised);
    border-left: 1px solid var(--line-strong);
    box-shadow: var(--shadow-lifted);
    animation: preview-in 320ms var(--ease-out) backwards;
}

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .preview {
        background-color: var(--card-frosted);
        -webkit-backdrop-filter: var(--blur-soft);
        backdrop-filter: var(--blur-soft);
    }
}

/* Nothing behind the sheet scrolls, on any screen: only the sheet's own body does. A finger or
   wheel on the scrim would otherwise move the shelf under it, the scrim having nothing to scroll. */
html:has(.preview:not(.is-leaving)) {
    overflow: hidden;
}

.preview__scrim {
    touch-action: none;
}

/* Opened over Settings' edition search, itself a sheet on a phone (22, its scrim 21): one layer
   up, so this scrim dims that sheet and this sheet sits over it. */
.preview__scrim--over,
.preview__scrim--over:hover:not(:disabled) {
    z-index: 23;
}

.preview--over {
    z-index: 24;
}

/* The words of SwitchEditionFoot: what the switch carries, and that this is the edition already. */
.preview__note,
.preview__current {
    margin: 0;
    text-align: center;
}

body:has(.preview) .search__fab {
    visibility: hidden;
}

@keyframes preview-in {
    from { transform: translateX(2rem); opacity: 0; }
    to { transform: none; opacity: 1; }
}

/* Leaving the way it came (js/motion.js): back off to the right, the scrim clearing with it. */
@keyframes preview-out {
    to { transform: translateX(2rem); opacity: 0; }
}

.preview.is-leaving {
    animation: preview-out 200ms var(--ease) forwards;
}

.preview__scrim.is-leaving,
.preview__scrim.is-leaving:hover:not(:disabled) {
    animation: clear 200ms var(--ease) forwards;
}

.preview__bar {
    display: flex;
    justify-content: flex-end;
    padding: 0.75rem 0.75rem 0;
}

.preview__close {
    display: grid;
    place-items: center;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    border-radius: 50%;
}

.preview__close svg {
    width: 0.95rem;
    height: 0.95rem;
}

.preview__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 0 1.5rem 1.25rem;
}

.preview__head {
    display: flex;
    gap: 1rem;
    align-items: flex-start;
}

.preview__cover {
    flex: 0 0 auto;
    width: 6.5rem;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: var(--shadow);
}

.preview__cover img {
    display: block;
    width: 100%;
}

.preview__ident {
    flex: 1 1 0;
    min-width: 0;
}

/* The page count on a line of its own: it is what the reader will count against once the book is
   theirs. Unknown, it says so in the quieter fill, and says it can be set later. */
.preview__pages {
    display: inline-flex;
    align-items: baseline;
    gap: 0.25rem;
    margin: 0.55rem 0 0;
    padding: 0.15rem 0.6rem;
    border-radius: 999px;
    background: var(--accent-soft);
    color: var(--accent);
    font-size: 0.8rem;
}

.preview__pages strong {
    font-size: 0.88rem;
}

.preview__pages--unknown {
    border-radius: var(--radius-sm);
    background: var(--fill);
    color: var(--ink-soft);
}

.preview .book__edition {
    margin-top: 1.1rem;
}

.preview__foot {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    padding: 0.9rem 1.5rem calc(1.1rem + env(safe-area-inset-bottom));
    border-top: 1px solid var(--line);
}

.preview__foot .notice {
    margin: 0;
}

.preview__statuses {
    display: flex;
    gap: 0.25rem;
    padding: 0.25rem;
    border-radius: 12px;
    background: var(--fill);
}

.preview__status {
    display: inline-flex;
    flex: 1 1 0;
    align-items: center;
    justify-content: center;
    gap: 0.45rem;
    min-height: 2.75rem;
    padding: 0 0.5rem;
    background: transparent;
    color: var(--ink-soft);
    font-size: 0.9rem;
    font-weight: 600;
    white-space: nowrap;
}

/* The one picked is washed in its own status's colour and ringed in it. */
.preview__status[aria-pressed="true"],
.preview__status[aria-pressed="true"]:hover:not(:disabled) {
    background: color-mix(in srgb, var(--dot) 16%, transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--dot) 55%, transparent);
    color: var(--ink);
}

@media (max-width: 560px) {
    /*
        The search dock's kind of sheet (sheet.js): dragged by the strip across its top between a
        peek, the half and nearly all the screen. The stylesheet paints it at the half, which is
        where the script opens it, so the two agree from the first frame. The peek is the handle
        and the foot — enough to add the book without its details in the way. Only the height
        eases; overflow is hidden so a peeked sheet clips its body rather than spilling it.
    */
    .preview {
        --sheet-peek: 172px;

        top: auto;
        left: 0;
        width: auto;
        height: 55dvh;
        overflow: hidden;
        border-left: 0;
        border-top: 1px solid var(--line-strong);
        border-radius: 22px 22px 0 0;
        transition: height 260ms var(--ease-out);
        animation-name: dock;
        animation-duration: 260ms;
    }

    /* Close sits at the right end of the handle's strip rather than on a row of its own. */
    .preview__bar {
        display: contents;
    }

    .preview__close {
        position: absolute;
        top: 0.5rem;
        right: 0.75rem;
        z-index: 1;
    }

    .preview__body {
        padding: 0.75rem 1rem 1rem;
    }

    /* Peeked, the sheet is the way to add the book and nothing else: whatever of the details
       would fit in the sliver between the handle and the foot is only a cut-off edge. */
    .preview[data-sheet="peek"] .preview__body {
        visibility: hidden;
    }

    /* Off the foot of the screen, as the other sheets go. */
    .preview.is-leaving {
        animation: undock 220ms cubic-bezier(0.4, 0, 1, 1) forwards;
    }

    /* The head set like the Book page's on a phone: a title page, the cover centred over the
       title, the authors, the page count and the chips. The description stays set left. */
    .preview__head {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        justify-items: center;
        text-align: center;
    }

    .preview__ident {
        display: flex;
        flex-direction: column;
        align-items: center;
        width: 100%;
    }

    .preview__cover {
        width: 5.5rem;
    }

    .preview .book__title {
        font-size: 1.35rem;
    }

    .preview .book__authors {
        font-size: 0.92rem;
    }

    .preview .book__reveal--chips {
        align-self: stretch;
    }

    .preview .book__categories {
        justify-content: center;
        margin-top: 0.75rem;
    }

    .preview__status {
        font-size: 0.85rem;
        gap: 0.35rem;
    }

    .preview__foot {
        padding: 0.75rem 1rem calc(1rem + env(safe-area-inset-bottom));
    }
}

/* Already on the shelf: dimmed, and offering nothing to add. */
.lib-card--mine {
    opacity: 0.6;
}

/* Not a page-level action but a row's: an outline, because the library is offering, not shouting. */
.button--add {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    flex: 0 0 auto;
    padding: 0.4rem 0.75rem;
    font-size: 0.85rem;
    line-height: 1.35;
    font-weight: 550;
    background: transparent;
    box-shadow: inset 0 0 0 1px var(--line-strong);
    color: var(--ink);
    white-space: nowrap;
}

.button--add svg {
    width: 0.85em;
    height: 0.85em;
}

.button--add:hover:not(:disabled) {
    background: var(--accent);
    color: var(--accent-ink);
    box-shadow: none;
}

/* The same size and shape of thing as the Add button it stands in for — same type, same padding,
   so a column of results keeps one baseline whether a book is owned or not. Only the fill differs. */
.onshelf {
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
    padding: 0.4rem 0.75rem;
    border-radius: var(--radius-sm);
    background: var(--fill);
    font-size: 0.85rem;
    line-height: 1.35;
    font-weight: 550;
    color: var(--ink-soft);
    white-space: nowrap;
}

/* Shown when the shelf has nothing and the library has not been asked yet: the one thing to do. */
.library-offer {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.7rem;
    padding: 0.5rem 0 0.75rem;
    color: var(--ink-soft);
}

.library-offer p {
    margin: 0;
}

/* One field with a magnifier drawn into its left end. The label is the hit area for the icon. */
.search__field {
    position: relative;
    display: flex;
    align-items: center;
    flex: 1 1 auto;
    min-width: 0;
}

.search__field svg {
    position: absolute;
    left: 0.8rem;
    width: 1rem;
    height: 1rem;
    color: var(--ink-faint);
    pointer-events: none;
}

.search__field input {
    width: 100%;
    padding: 0.55rem 2.4rem 0.55rem 2.4rem;
}

/* The cross at the field's right end, there only once there is something to clear. */
.search__clear {
    position: absolute;
    right: 0.35rem;
    width: 1.8rem;
    height: 1.8rem;
    padding: 0;
    display: inline-grid;
    place-items: center;
    border-radius: 50%;
    background: transparent;
    color: var(--ink-faint);
}

.search__clear:hover:not(:disabled) {
    background: var(--fill);
    color: var(--ink);
}

.search__clear svg {
    width: 0.9rem;
    height: 0.9rem;
}

.search__inputs .button--primary {
    font-weight: 600;
}

/* The way into the phone's search dock. On a wider screen the panel is simply on the page. */
.search__fab {
    display: none;
}

.search__scrim {
    display: none;
}

.search__inputs input {
    flex: 1 1 8rem;
}

.search__inputs button {
    flex: 0 0 auto;
    padding-inline: 1.25rem;
}

.results {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.result {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    padding: 0.6rem;
    border-radius: var(--radius-sm);
    background: var(--fill);
    transition: background-color var(--quick) var(--ease);
}

.result:hover {
    background: var(--fill-hover);
}

/*
    Results arrive rather than appear. A search replaces the whole list at once, and a block of
    rows switched on in one frame reads as the page having jumped; coming in a beat apart reads as
    the answer being laid out. Home.razor keys each row by its book, so a fresh search builds new
    elements and this plays again, and hands down its position as --at.

    Capped at the eighth row: a full page is forty books, and the fortieth arriving two seconds
    after the first is not a flourish, it is a wait. Past the cap they come in together.
*/
.result {
    animation: rise 420ms var(--ease-out) both;
    animation-delay: calc(min(var(--at, 0), 8) * 45ms);
}

.result img {
    width: 2.5rem;
    display: block;
}

.result__cover-placeholder {
    width: 2.5rem;
}

.result__body {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.result__body span {
    font-size: 0.85rem;
}

/* Smaller than a page-level action: it is one row's button, repeated down the list. */
.result button {
    flex: 0 0 auto;
    padding: 0.4rem 0.8rem;
    font-size: 0.9rem;
}

.pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    margin-top: 1.1rem;
    font-size: 0.9rem;
}

/* Adding by hand ---------------------------------------------------------- */

/* A form, not a row of controls: labels sit above their boxes and the boxes run the full width,
   so five fields read as a column rather than as five differently indented lines. */
.by-hand .field {
    display: block;
    margin-bottom: 0.85rem;
}

.by-hand .field label {
    display: block;
    margin-bottom: 0.25rem;
    font-weight: 550;
    color: var(--ink);
}

.by-hand .field input {
    width: 100%;
}

.by-hand .field input[type="number"] {
    width: 8rem;
}

.by-hand h3 {
    margin-bottom: 0.15rem;
}

.by-hand > .hint {
    margin: 0 0 1.1rem;
}

/* Page-level messages ----------------------------------------------------- */

/* Devices ----------------------------------------------------------------- */

/* The mint form is the search box's row: a field and the one filled action beside it. */
.devices__mint {
    display: flex;
    gap: 0.5rem;
    margin: 0 0 1rem;
}

.devices__mint input {
    flex: 1 1 auto;
    min-width: 0;
}

.devices__mint .button--primary {
    flex: 0 0 auto;
    padding-inline: 1.25rem;
    font-weight: 600;
}

/* The shelf's stack of cards, as a list. */
.devices .entries {
    list-style: none;
    margin: 0;
    padding: 0;
}

/*
    A device on the shelf: glyph, name and dates in the book card's places, and its state on the
    right where the card's actions go. A grid rather than the entry's flex row, because the name
    and the dates stack in one column while the glyph and the state span both.
*/
.device {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    gap: 0.15rem 1rem;
    align-items: center;
    padding: 0.85rem 1rem;
}

.device__glyph {
    grid-column: 1;
    grid-row: 1 / span 2;
    display: grid;
    place-items: center;
    width: 2.6rem;
    height: 3.2rem;
    border-radius: 6px;
    background: var(--fill);
    color: var(--ink-soft);
}

.device__glyph svg {
    width: 1.4rem;
    height: 1.9rem;
}

.device__name {
    grid-column: 2;
    grid-row: 1;
    margin: 0;
    font-weight: 650;
    line-height: 1.25;
}

.device__when {
    grid-column: 2;
    grid-row: 2;
    margin: 0;
    font-size: 0.83rem;
    font-variant-numeric: tabular-nums;
    color: var(--ink-soft);
}

.device__state {
    grid-column: 3;
    grid-row: 1 / span 2;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.35rem;
}

/* The revoke question drops under the whole row rather than squeezing in beside the name. */
.device__question {
    grid-column: 1 / -1;
    margin-top: 0.5rem;
}

/* On a phone the state goes under the dates, with the pill first and Revoke at the end. */
@media (max-width: 480px) {
    .device__state {
        grid-column: 2;
        grid-row: 3;
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
        margin-top: 0.4rem;
    }
}

/* State said in a word and a colour: the accent for talking, amber for quiet, nothing for never. */
.pill {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.12rem 0.55rem;
    border-radius: 999px;
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    white-space: nowrap;
}

.pill i {
    width: 0.45rem;
    height: 0.45rem;
    border-radius: 50%;
    background: currentColor;
}

.pill--syncing {
    background: var(--accent-soft);
    color: var(--accent);
}

.pill--quiet {
    background: var(--warn-soft);
    color: var(--warn);
}

.pill--neverused,
.pill--faint {
    background: var(--fill);
    color: var(--ink-soft);
}

.pill--bad {
    background: var(--danger-soft);
    color: var(--danger);
}

/* Your reading ------------------------------------------------------------ */

.stats {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

.stats__heading {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}

.stats__heading h1 {
    margin: 0;
}

.stats__years {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.25rem;
    border-radius: 12px;
    background: var(--fill);
}

.stats__year {
    min-width: 3.5rem;
    text-align: center;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.stats__step {
    display: grid;
    place-items: center;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: 9px;
    color: var(--ink-soft);
    transition: background-color var(--quick) var(--ease), color var(--quick) var(--ease);
}

.stats__step:hover {
    background: var(--fill-hover);
    color: var(--ink);
}

/* No next year yet: the arrow stays, faint, so the year sits in the middle. */
.stats__step--none {
    color: var(--line-strong);
}

.stats__step--none:hover {
    background: none;
    color: var(--line-strong);
}

.stats__step svg {
    width: 1rem;
    height: 1rem;
}

.stats__lede {
    margin: -0.75rem 0 0;
    color: var(--ink-soft);
}

.stats__tile,
.stats__day,
.stats__weekdays,
.calendar {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--card-sheer);
}

/*
    The calendar: a column a week, Monday at the top, each day a square. The squares are
    buttons — picking one says what was read that day — and they size with the page, so a
    year always spans the width.
*/
.calendar {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    padding: 1.25rem 1.25rem 1rem;
}

.calendar__bar {
    display: none;
}

.calendar__body {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 0.5rem;
}

.calendar__labels,
.calendar__week {
    display: grid;
    grid-template-rows: 1.1rem repeat(7, auto);
    gap: 3px;
}

.calendar__labels span {
    display: flex;
    align-items: center;
    font-size: 0.7rem;
    color: var(--ink-faint);
}

.calendar__weeks {
    display: flex;
    gap: 3px;
}

.calendar__week {
    flex: 1 1 0;
    min-width: 0;
}

.calendar__month {
    width: 0;
    overflow: visible;
    white-space: nowrap;
    font-size: 0.72rem;
    color: var(--ink-faint);
}

/* A day's colour is --day, so the buttons' own hover and press fills cannot repaint it. */
.calendar__day {
    --day: var(--fill);

    display: block;
    aspect-ratio: 1;
    width: 100%;
    padding: 0;
    border: 0;
    border-radius: 3px;
    background: var(--day);
}

button.calendar__day,
button.calendar__day:hover:not(:disabled),
button.calendar__day:active:not(:disabled) {
    background: var(--day);
    transform: none;
    cursor: pointer;
    transition: box-shadow var(--quick) var(--ease);
}

button.calendar__day:hover:not(:disabled) {
    box-shadow: 0 0 0 1.5px var(--ink-faint);
}

.calendar__day--0 { --day: var(--fill); }
.calendar__day--1 { --day: color-mix(in srgb, var(--status-alive) 28%, transparent); }
.calendar__day--2 { --day: color-mix(in srgb, var(--status-alive) 50%, transparent); }
.calendar__day--3 { --day: color-mix(in srgb, var(--status-alive) 75%, transparent); }
.calendar__day--4 { --day: var(--status-alive); }

.calendar__day--on,
button.calendar__day--on:hover:not(:disabled) {
    box-shadow: 0 0 0 2px var(--card), 0 0 0 3.5px var(--ink);
}

.calendar__day--out {
    background: none;
}

.calendar__day--ahead {
    background: none;
    box-shadow: inset 0 0 0 1px var(--line);
}

.calendar__legend {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    font-size: 0.75rem;
    color: var(--ink-faint);
}

.calendar__scale {
    display: flex;
    align-items: center;
    gap: 3px;
}

.calendar__scale i {
    width: 0.75rem;
    height: 0.75rem;
    border-radius: 3px;
    background: var(--day);
}

.calendar__scale i:first-child {
    margin-left: 0.3rem;
}

.calendar__scale i:last-child {
    margin-right: 0.3rem;
}

.stats__below {
    display: grid;
    grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
    gap: 1rem;
    align-items: start;
}

.stats__day,
.stats__weekdays {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    padding: 1.1rem 1.25rem;
}

.stats__day {
    border-color: color-mix(in srgb, var(--status-alive) 40%, transparent);
    background: color-mix(in srgb, var(--status-alive) 7%, var(--card-sheer));
}

.stats__day h2,
.stats__weekdays h2 {
    margin: 0;
    font-size: 1rem;
}

.stats__day p,
.stats__weekdays p {
    margin: 0;
}

.stats__day-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
}

.stats__day-head span {
    font-size: 0.85rem;
    color: var(--status-alive);
    font-variant-numeric: tabular-nums;
}

.stats__books {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.stats__books li {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0 1rem;
    font-size: 0.9rem;
}

.stats__book {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 600;
}

.stats__book-pages {
    font-variant-numeric: tabular-nums;
    color: var(--ink-soft);
}

.stats__book-from {
    grid-column: 1 / -1;
    font-size: 0.78rem;
    color: var(--ink-faint);
}

.stats__bars {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 0.5rem;
    height: 6.5rem;
}

.stats__bar {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    align-items: center;
    gap: 0.35rem;
}

.stats__bar span {
    width: 100%;
    min-height: 3px;
    border-radius: 5px 5px 2px 2px;
    background: var(--status-alive);
}

.stats__bar em {
    font-style: normal;
    font-size: 0.72rem;
    color: var(--ink-faint);
}

.stats__weekdays p {
    font-size: 0.85rem;
}

.stats__tiles {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 1rem;
}

.stats__tile {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    padding: 1rem 1.25rem;
}

.stats__tile h2 {
    margin: 0;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.stats__tile strong {
    font-size: 1.75rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.stats__tile span {
    font-size: 0.82rem;
    color: var(--ink-soft);
}

@media (max-width: 900px) {
    .stats__below {
        grid-template-columns: minmax(0, 1fr);
    }

    .stats__tiles {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

/*
    A phone shows the last seventeen weeks, big enough to tap, and the whole year on asking —
    then at the same size, scrolling sideways, with today at the end.
*/
@media (max-width: 560px) {
    .stats__heading h1 {
        font-size: 1.6rem;
    }

    .calendar {
        padding: 1rem;
    }

    .calendar__bar {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: 1rem;
        font-size: 0.9rem;
        font-weight: 600;
    }

    .calendar__toggle {
        font-size: 0.85rem;
        font-weight: 500;
    }

    .calendar__week--early,
    .calendar__week--ahead {
        display: none;
    }

    /* Opened on the latest weeks by js/calendar.js, and scrolled back from there. */
    .calendar--whole .calendar__weeks {
        overflow-x: auto;
        overscroll-behavior-x: contain;
        padding-bottom: 0.25rem;
    }

    .calendar--whole .calendar__week {
        flex: 0 0 1.05rem;
    }

    .calendar--whole .calendar__week--early {
        display: grid;
    }

    .calendar__legend span:first-child {
        display: none;
    }

    .stats__tile {
        padding: 0.9rem 1rem;
    }

    .stats__tile strong {
        font-size: 1.35rem;
    }
}

/* Import ------------------------------------------------------------------ */

/*
    The way in is one big target: a sheer card with the file picker behind it, so the whole card
    is the button. The real input is visually hidden but still the thing focused and clicked, so
    the keyboard and a screen reader see an ordinary file field.
*/
.import__drop {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 1.4rem 1.5rem;
    border: 1px dashed var(--line-strong);
    border-radius: var(--radius);
    background: var(--card-sheer);
    cursor: pointer;
    transition: border-color var(--quick) var(--ease), background-color var(--quick) var(--ease);
}

.import__drop:hover,
.import__drop:focus-within {
    border-color: var(--accent);
    background: var(--accent-soft);
}

.import__drop:focus-within {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.import__drop-glyph {
    display: grid;
    place-items: center;
    width: 2.8rem;
    height: 2.8rem;
    border-radius: 50%;
    background: var(--accent-soft);
    color: var(--accent);
}

.import__drop-glyph svg {
    width: 1.4rem;
    height: 1.4rem;
}

.import__drop-text strong {
    display: block;
    font-weight: 650;
}

.import__drop-text span {
    display: block;
    font-size: 0.85rem;
    color: var(--ink-soft);
}

/*
    Two panes. On the left, a glass card holding everything to do, kept in view; on the right,
    the books, in a list that scrolls on its own. The page is wider than the others to fit both.
    Your reading is as wide, to fit a year's fifty-three weeks across.
*/
main:has(.import),
main:has(.stats) {
    max-width: 70rem;
}

.import {
    --import-top: var(--header-offset);

    display: grid;
    grid-template-columns: 21rem minmax(0, 1fr);
    gap: 1.75rem;
    align-items: start;
}

.import__side {
    position: sticky;
    top: var(--import-top);
    display: flex;
    flex-direction: column;
    gap: 1.1rem;
    padding: 1.25rem;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius);
    background: var(--card-raised);
    box-shadow: var(--shadow-lifted);
}

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .import__side {
        background-color: var(--card-frosted);
        -webkit-backdrop-filter: var(--blur-soft);
        backdrop-filter: var(--blur-soft);
    }
}

.import__side h1 {
    margin: 0;
    font-size: 1.5rem;
}

.import__from {
    margin: 0;
    padding: 0;
    border: 0;
}

.import__sources {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.import__from legend {
    margin-bottom: 0.5rem;
    padding: 0;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.import__source {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.65rem;
    min-height: 2.75rem;
    padding: 0 0.75rem;
    border-radius: var(--radius-sm);
    background: var(--fill);
    cursor: pointer;
    transition: background-color var(--quick) var(--ease), box-shadow var(--quick) var(--ease);
}

.import__source input {
    margin: 0;
    accent-color: var(--accent);
}

.import__source--on {
    background: var(--accent-soft);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
    font-weight: 600;
}

.import__steps {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    margin: 0;
    padding-left: 1.1rem;
    font-size: 0.82rem;
    line-height: 1.45;
    color: var(--ink-soft);
}

.import__side .import__drop {
    padding: 0.9rem 1rem;
}

.import__side .notice {
    margin: 0;
}

.import__file {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.55rem 0.6rem 0.55rem 0.75rem;
    border: 1px dashed var(--line-strong);
    border-radius: var(--radius-sm);
}

.import__file svg {
    flex: 0 0 auto;
    width: 1.1rem;
    height: 1.1rem;
    color: var(--accent);
}

.import__file-name {
    flex: 1 1 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 0.85rem;
}

.import__change {
    padding: 0.3rem 0.65rem;
    border-radius: 8px;
    background: var(--fill);
    font-size: 0.8rem;
}

.import__counts {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    row-gap: 0.45rem;
    margin: 0;
    font-size: 0.9rem;
}

.import__counts dt {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.import__counts dd {
    margin: 0;
    padding-left: 0.75rem;
    font-variant-numeric: tabular-nums;
}

.import__counts .import__counts-all {
    padding-top: 0.45rem;
    border-top: 1px solid var(--line);
    font-weight: 600;
}

.import__go {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.import__go .button--primary {
    min-height: 2.9rem;
    border-radius: 12px;
    font-weight: 600;
}

.import__go .hint {
    margin: 0;
    text-align: center;
}

/* The way back out, under a rule at the foot of the card. */
.import__export {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    padding-top: 1.1rem;
    border-top: 1px solid var(--line);
}

.import__export h2 {
    margin: 0;
    font-size: 1.05rem;
}

.import__export p {
    margin: 0;
}

.import__export button {
    min-height: 2.6rem;
    border-radius: 12px;
}

/* The books: a list that scrolls by itself, down to the foot of the screen and no further. */
.import__list {
    height: calc(100dvh - var(--import-top) - 1.5rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--card-sheer);
}

.import__empty {
    max-width: 26rem;
    margin: 3rem auto;
    padding: 0 1.5rem;
    text-align: center;
    line-height: 1.55;
    color: var(--ink-soft);
}

/* Each status heads its own books and stays up top while they scroll beneath it. */
.import__group {
    position: sticky;
    top: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0;
    padding: 0.6rem 1.1rem;
    border-bottom: 1px solid var(--line);
    background: var(--card);
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.import__progress {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.import__count {
    margin: 0;
    color: var(--ink-soft);
}

.import__count strong {
    color: var(--ink);
    font-weight: 650;
}

/* The books, one ruled row each, on one grid so the columns line up — a ledger to check over. */
.import__rows {
    margin: 0;
    padding: 0;
    list-style: none;
}

.import__row {
    display: grid;
    grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) 7.5rem;
    gap: 0.2rem 1rem;
    align-items: center;
    min-height: 2.6rem;
    padding: 0.35rem 1.1rem;
    border-bottom: 1px solid var(--line);
    font-size: 0.9rem;
}

.import__title {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 600;
}

.import__by {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--ink-soft);
}

.import__status {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5rem;
    font-size: 0.85rem;
    font-variant-numeric: tabular-nums;
    color: var(--ink-soft);
    white-space: nowrap;
}

/* While it runs, the last column holds an outcome and sometimes a note, so it takes their width. */
.import__rows--results .import__row {
    grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) auto;
}

.import__note {
    white-space: normal;
    font-size: 0.8rem;
}

.import__row--faded .import__title {
    color: var(--ink-soft);
}

/*
    Narrower than two panes can share — a small tablet, half a split screen — they stack: what to
    do, then the books, in a list a little over half a screen tall.
*/
@media (max-width: 900px) {
    .import {
        grid-template-columns: minmax(0, 1fr);
        gap: 1rem;
    }

    .import__side {
        position: static;
    }

    .import__list {
        height: 60dvh;
    }
}

/*
    A phone drops the card around what to do, and the button leaves it for a frosted bar at the
    foot, so it is always a thumb away.
*/
@media (max-width: 560px) {
    main:has(.import__go) {
        padding-bottom: 7rem;
    }

    .import__side {
        padding: 0;
        border: 0;
        background: none;
        box-shadow: none;
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
    }

    .import__sources {
        flex-direction: row;
    }

    .import__source {
        flex: 1 1 0;
        justify-content: center;
        padding: 0 0.5rem;
    }

    .import__source input {
        position: absolute;
        opacity: 0;
        pointer-events: none;
    }

    .import__source:has(input:focus-visible) {
        outline: 2px solid var(--accent);
        outline-offset: 2px;
    }

    .import__counts {
        display: none;
    }

    .import__go {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 10;
        padding: 0.75rem 1rem calc(0.9rem + env(safe-area-inset-bottom));
        border-top: 1px solid var(--line-strong);
        background: var(--card-raised);
    }

    @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
        .import__go {
            background-color: var(--card-frosted);
            -webkit-backdrop-filter: var(--blur-soft);
            backdrop-filter: var(--blur-soft);
        }
    }

    .import__group {
        padding: 0.55rem 0.9rem;
    }

    .import__row,
    .import__rows--results .import__row {
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas: "title status" "by status";
        gap: 0.1rem 0.75rem;
        padding: 0.5rem 0.9rem;
    }

    .import__title { grid-area: title; }
    .import__by { grid-area: by; font-size: 0.82rem; }
    .import__status { grid-area: status; font-size: 0.78rem; }
}

.import__actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.import__actions .hint {
    flex-basis: 100%;
}

/* A link dressed as the one filled action. */
.import__shelf {
    display: inline-flex;
    align-items: center;
    padding: 0.45rem 1.25rem;
    border-radius: var(--radius-sm);
    background: var(--accent);
    color: var(--accent-ink);
    font-weight: 600;
    text-decoration: none;
}

.import__shelf:hover {
    background: var(--accent-hover);
}

/*
    The token, on its one and only showing. Monospace and wrapped, so a long secret fits a phone
    and can be selected whole; picked out in the accent so the eye lands on it before the notice
    above says why.
*/
.token-reveal__secret {
    display: block;
    margin: 0.6rem 0 0.9rem;
    padding: 0.6rem 0.75rem;
    border-radius: var(--radius-sm);
    background: var(--card);
    font-size: 0.9rem;
    overflow-wrap: anywhere;
    user-select: all;
}

.notice {
    padding: 0.7rem 0.95rem;
    background: var(--accent-soft);
    border-radius: var(--radius-sm);
    margin: 1rem 0 0;
}

/* Something went wrong, as opposed to something worth knowing. The accent reads as reassuring,
   which is the wrong note for the reason a sign-in failed. */
.notice--bad {
    background: var(--danger-soft);
    color: var(--danger);
}

.empty {
    padding: 2rem 1.5rem;
    text-align: center;
    color: var(--ink-soft);
    border: 1px dashed var(--line-strong);
    border-radius: var(--radius);
}

/* Both take the place of something that was just there — the ghosts, a form, a list — so they
   come in the way the cards do rather than being switched on in the gap. */
.notice,
.empty {
    animation: rise 320ms var(--ease-out) backwards;
}

#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0;
    width: 100%;
    padding: 0.6rem 1.25rem;
    background: var(--danger-soft);
    color: var(--danger);
    border-top: 1px solid var(--danger);
}

/* Translucency ------------------------------------------------------------ */

/*
    Only where the blur is actually supported. Everywhere else the header and the search keep the
    opaque --card they are given above, because a translucent panel with nothing blurring behind it
    is text on top of whatever happens to be underneath. The shelf's cards are not in here: behind
    them is only the mesh, so they are sheer everywhere and blur nothing — see .entry.
*/
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    /*
        The header's material is a pane behind it, not the header itself. An element with a
        backdrop filter is the backdrop root for everything inside it: a descendant's own filter
        can sample only that element's box. The account menu hangs below the header, and with the
        blur on the header the menu's blur sampled nothing — the shelf's text came through the
        menu untouched, only tinted. A pseudo-element carries the header's blur instead, so the
        header itself is not a root and the menu blurs the page it is actually over.

        The header is sticky with a z-index, so it is a stacking context of its own and the pane
        at -1 stays inside it: behind the bar's contents, above the page.
    */
    header {
        background: transparent;
    }

    header::before {
        content: "";
        position: absolute;
        inset: 0;
        z-index: -1;
        background: var(--card-raised);
        -webkit-backdrop-filter: var(--blur);
        backdrop-filter: var(--blur);
    }

    /* The header's menus and the status list are frosted glass over the shelf: sheerer than the
       header and blurred less, so the shelf shows through as shapes that cannot be read. */
    .account__menu,
    .goal__menu,
    .status__menu {
        background: var(--card-frosted);
        -webkit-backdrop-filter: var(--blur-soft);
        backdrop-filter: var(--blur-soft);
    }

    /* Controls take the tint but not a blur of their own: they sit on surfaces that are already
       blurring, and blurring an already-blurred backdrop only muddies it. Not <select>: see above. */
    input {
        background-color: var(--card-translucent);
    }
}

/* Motion ------------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        transition-duration: 0.01ms !important;
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
    }

    /* The deck itself stops turning too, which is a decision the component has to make rather
       than the stylesheet: see CoverFlow.razor. Killing the transitions here would otherwise
       leave it snapping from book to book, which is more startling than the sliding was. */

    button:active:not(:disabled) {
        transform: none;
    }
}

@media (max-width: 40rem) {
    main {
        --main-pad-top: 1.25rem;

        /* Room under the last card for the + button to float over without covering it. */
        padding: var(--main-pad-top) 1rem 5.5rem;
    }

    /* With the dock open the + is gone, and that room would sit under the dock as a bare strip
       of page between it and the bottom edge once the shelf is scrolled to its end. */
    main:has(.search--open) {
        padding-bottom: 0;
    }

    .hero__card {
        width: calc(100% - 2rem);
        padding-inline: 1rem;
    }

    /*
        One row, always: the brand takes up the slack and whatever is at the other end -- the
        monogram, or the sign-in control when nobody is -- travels against the right edge. The bar
        wrapping onto a second row cost 104px of an 844px screen once.
    */
    header {
        gap: 0.6rem;
        flex-wrap: nowrap;
        /* Lines the bar up with the page under it: the brand sat 24px in while the cards below
           started at 16px, so the two disagreed down the whole left edge. */
        padding: 0.7rem 1rem;
    }

    header > a:first-child {
        margin-right: auto;
    }

    /* On a phone the figure alone is the badge: the bar has no room and the figure says it. */
    .goal__track {
        display: none;
    }

    .goal__badge {
        padding-inline: 0.4rem;
    }

    /* The way in keeps its size whatever the space beside it does. */
    header > button {
        flex: 0 0 auto;
    }

    /*
        On a phone the card has a banner: the jacket blurred into a band across its top, the
        status on the band at the right, and the cover overlapping it at the left with the title
        and authors beside it, all inside the band's colour. The figure, the bar and the actions
        run full width beneath, so nothing is left empty under the cover. --cover is the jacket's
        address, set on the card by ShelfEntry. How the band is drawn is the theme's (--band-*
        on :root). The Devices page's cards are entries too, and have no jacket to band with.

        The body is display: contents so every piece sits on the card's own grid. Spacing is
        margins rather than row gaps, so a row with nothing in it (no device line) costs nothing.

        The band is painted over the card's background, never under it: under it, it would show
        through the sheer glass only while nothing on the card starts a stacking context, and the
        card's rise-in does, so the band would brighten for the length of the arrival and dim at
        its end. Over it, the pieces that sit on the band are lifted above it by being positioned
        themselves; nothing else reaches the band, and the sheets the card opens are left alone.
    */
    .entry:not(.entry--pending) {
        position: relative;
        display: grid;
        grid-template-columns: 4.75rem minmax(0, 1fr);
        grid-template-areas:
            "cover  status"
            "cover  head"
            "fig    fig"
            "track  track"
            "device device"
            "acts   acts";
        grid-template-rows: auto 1fr auto auto auto auto;
        column-gap: 0.9rem;
        padding: 0.7rem 0.9rem 0.9rem;
    }

    .entry:not(.entry--pending, .device)::before {
        content: "";
        position: absolute;
        inset: calc(-1 * var(--band-bleed)) calc(-1 * var(--band-bleed)) auto;
        height: calc(var(--band-bleed) + var(--band-fade-to));
        background:
            linear-gradient(var(--band-veil), var(--band-veil)),
            var(--cover, var(--accent-soft)) center / cover;
        filter: blur(var(--band-blur)) saturate(var(--band-saturate));
        opacity: var(--band-opacity);
        clip-path: inset(var(--band-bleed) var(--band-bleed) 0 round calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0);
        mask-image: linear-gradient(to bottom,
            #000 calc(var(--band-bleed) + var(--band-fade-from)),
            transparent calc(var(--band-bleed) + var(--band-fade-to)));
        pointer-events: none;
    }

    /* The band takes the card's standing, as the cover does: grey set aside, warm read. */
    .entry--parked:not(.entry--pending)::before {
        filter: blur(var(--band-blur)) grayscale(1);
    }

    .entry--finished:not(.entry--pending)::before {
        filter: blur(var(--band-blur)) sepia(0.75) saturate(var(--band-warm-saturate));
    }

    .entry:not(.entry--pending) .entry__body {
        display: contents;
    }

    .entry:not(.entry--pending) .entry__cover,
    .entry:not(.entry--pending) .entry__head,
    .entry:not(.entry--pending) .progress__figure,
    .entry:not(.entry--pending) .progress__device,
    .entry:not(.entry--pending) .entry__actions,
    .entry:not(.entry--pending) .entry__reasons {
        position: relative;
    }

    .entry:not(.entry--pending) .entry__cover {
        grid-area: cover;
        align-self: start;
        width: auto;
        min-height: 0;
        margin-top: 1.4rem;
        border-radius: 8px;
        box-shadow: 0 6px 16px rgb(0 0 0 / 22%);
    }

    .entry:not(.entry--pending) .entry__cover > * {
        position: static;
        height: auto;
    }

    .entry:not(.entry--pending) .entry__cover > .entry__cover-placeholder {
        aspect-ratio: 2 / 3;
    }

    .entry:not(.entry--pending) .entry__status {
        grid-area: status;
        justify-self: end;
    }

    /* On the band rather than on the card, so frosted enough to read against any jacket. */
    .entry:not(.entry--pending) .status__trigger {
        background: var(--card-on-band);
        box-shadow: 0 1px 3px rgb(0 0 0 / 12%);
    }

    .entry:not(.entry--pending) .entry__head {
        grid-area: head;
        align-self: end;
        margin-top: 0.5rem;
    }

    .entry:not(.entry--pending) .progress__figure {
        grid-area: fig;
        margin-top: 0.75rem;
    }

    .entry:not(.entry--pending) .progress__track {
        grid-area: track;
        margin-top: 0.55rem;
    }

    .entry:not(.entry--pending) .progress__device {
        grid-area: device;
        margin-top: 0.45rem;
    }

    .entry:not(.entry--pending) .entry__actions {
        grid-area: acts;
        margin-top: 0.75rem;
    }

    /* The ghosts keep the plain shape: a small cover beside the bones. */
    .entry--pending {
        gap: 0.85rem;
        padding: 0.9rem;
    }

    .entry--pending .entry__cover {
        width: 3.25rem;
        min-height: 0;
        border-radius: 8px;
    }

    .entry--pending .entry__cover > * {
        position: static;
        height: auto;
        aspect-ratio: 2 / 3;
    }

    .entry--pending .entry__body {
        padding: 0;
    }

    /*
        On a phone the head is set like a title page: the cover centred and a size up, since it
        is the centrepiece now rather than a thumbnail beside the text; the status directly
        beneath it, a label on the jacket; then the title, the authors and the chips, all
        centred. The description below stays set left — a centred paragraph is harder to read.
        The way back is the one thing above all this, alone in the top-left corner.
    */
    .book__head {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        justify-items: center;
        text-align: center;
        row-gap: 0;
    }

    .book__ident {
        display: contents;
    }

    .book__cover {
        grid-row: 1;
        width: 5.5rem;
    }

    .book__status {
        grid-row: 2;
        margin: 0.7rem 0 0.75rem;
    }

    .book__title {
        grid-row: 3;
        font-size: 1.35rem;
    }

    .book__authors {
        grid-row: 4;
        font-size: 0.92rem;
    }

    .book__reveal--chips {
        grid-row: 5;
        justify-self: stretch;
    }

    .book__categories {
        justify-content: center;
        margin-top: 0.75rem;
    }

    .field label {
        min-width: 5rem;
    }

    /*
        Below this width the shelf stops being a page with rows on it and becomes a column, and
        anything that wraps by itself wraps badly: flex fills the line and leaves the remainder
        stranded on the next one. Every group here is given an explicit shape instead, so each row
        is either one full-width thing or two equal halves, and the left and right edges line up
        all the way down. Measured at 390px, which is where the ragged versions were found.
    */

    /*
        The filters are a row of underlined tabs you swipe, not pills that wrap: six labels do
        not fit across a phone at any pill size without one of them breaking in the middle. Bled
        to the screen's edges so the last tab can hang off it and say there is more.

        They swipe sideways and nothing else. overflow-x on its own makes overflow-y auto too,
        and the tabs' underline hangs a pixel below the strip, so the strip scrolled up and down
        by that pixel under a thumb and floated; and a swipe that reached either end carried on
        into the page. The strip is held on the vertical and keeps its swipes to itself, and its
        rule is drawn inside it rather than under it, so the chosen tab's underline covers the
        rule without hanging out of the strip to do it.
    */
    .filters {
        flex-wrap: nowrap;
        overflow-x: auto;
        overflow-y: hidden;
        overscroll-behavior-x: contain;
        gap: 1.1rem;
        margin-inline: -1rem;
        padding-inline: 1rem;
        box-shadow: inset 0 -1px 0 var(--line);
        scrollbar-width: none;
    }

    .filters::-webkit-scrollbar {
        display: none;
    }

    /*
        The list under the tabs is swiped through them (js/shelfSwipe.js), and follows the
        finger while it does. Sideways the page has nowhere to scroll, so the browser is told as
        much: a drag that starts sideways is left to the script and is never also a scroll, which
        is what lets the script's listeners stay passive. Up and down, and pinching, are the
        page's as ever; the sheets the cards open scroll by themselves, so this stops at them.
    */
    .shelf > .entries,
    .shelf > .empty {
        touch-action: pan-y pinch-zoom;
    }

    .filters .chip {
        flex: 0 0 auto;
        padding: 0.45rem 0 0.6rem;
        border-radius: 0;
        border-bottom: 2px solid transparent;
        background: none;
        color: var(--ink-soft);
        font-size: 0.9rem;
        font-weight: 550;
        white-space: nowrap;
    }

    .filters .chip:hover:not(:disabled) {
        background: none;
        color: var(--ink);
    }

    .filters .chip--on,
    .filters .chip--on:hover:not(:disabled) {
        background: none;
        color: var(--ink);
        border-bottom-color: var(--accent);
    }

    /*
        The card's one action takes a line of its own, full width and a size up, and the three
        quiet ones share the line beneath it, centred, with a dot between each. On a phone the
        thumb wants one big target, and the rest of the row was only ever housekeeping.
    */
    .entry__actions {
        justify-content: center;
        align-items: center;
        gap: 0.2rem;
        margin-left: 0;
        color: var(--ink-faint);
    }

    /* Still the big target a thumb wants, but in the wide screen's voice: the accent as a soft
       tint and a word, not a slab of solid fill under the figure. */
    .entry__actions .button--log {
        flex: 1 1 100%;
        justify-content: center;
        margin: 0 0 0.25rem;
        padding: 0.7rem 1rem;
        font-size: 1.02rem;
        border-radius: 12px;
        background: var(--accent-soft);
        color: var(--accent);
        box-shadow: none;
    }

    .entry__actions .button--log:hover:not(:disabled) {
        background: color-mix(in srgb, var(--accent) 22%, transparent);
    }

    .entry__actions .link-button + .link-button::before {
        content: "·";
        margin-right: 0.45rem;
        color: var(--ink-faint);
    }

    /*
        With its form open the big button is the way back out, and says so quietly: an outline in
        place of the fill, so the form beneath is the one thing on the card in the accent.
    */
    .entry__actions .button--log--open {
        background: transparent;
        box-shadow: inset 0 0 0 1px var(--line-strong);
        color: var(--ink-soft);
    }

    /*
        The session form as a small, even grid rather than three ragged rows. Read and For share
        the first row, the date has the second to itself, and every label sits above its box in
        the same small capitals as the figure on the card, so the form reads in the card's voice.
        Units are set inside the box's right end instead of trailing it, which is what let the
        rows line up.
    */
    .session-form {
        display: grid;
        /* A track is free to be narrower than what sits in it: a field that reports a wide
           intrinsic size -- a date, in a locale that spells its months out -- shrinks with the
           card rather than carrying the grid past its edge. */
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        gap: 0.7rem 0.6rem;
    }

    .session-form .field {
        position: relative;
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: 0.3rem;
        min-width: 0;
    }

    .session-form .field:nth-child(1) { grid-column: 1; grid-row: 1; }
    .session-form .field:nth-child(3) { grid-column: 2; grid-row: 1; }
    .session-form .field:nth-child(2) { grid-column: 1 / -1; grid-row: 2; }

    /* With the stop slider in place of Read, the slider has the first row and the minutes the
       third: half a card was too narrow for the date beside them. */
    .session-form .stop { grid-column: 1 / -1; grid-row: 1; }
    .session-form:has(> .stop) .field:nth-child(3) { grid-column: 1 / -1; grid-row: 3; }

    .session-form .field label {
        font-size: 0.68rem;
        letter-spacing: 0.08em;
        text-transform: uppercase;
        color: var(--ink-faint);
    }

    .session-form .field input,
    .session-form .field input[type="number"] {
        width: 100%;
        padding: 0.65rem 0.8rem;
        font-variant-numeric: tabular-nums;
    }

    .session-form .field:has(.unit) input {
        padding-right: 3.6rem;
    }

    .session-form .unit {
        position: absolute;
        right: 0.8rem;
        bottom: 0.72rem;
        pointer-events: none;
    }

    /*
        One action, full width and in the accent, the same shape as the Log reading button it
        unfolded from. Logging a session needs no Cancel of its own -- the button above already is
        one -- but the same form correcting a session inside History has no such button, and
        keeps its link.
    */
    .session-form__actions {
        margin-top: 0.9rem;
    }

    .session-form__actions button:first-child {
        flex: 1 1 auto;
        padding: 0.7rem 1rem;
        border-radius: 12px;
        font-size: 1.02rem;
        font-weight: 600;
        background: var(--accent);
        color: var(--accent-ink);
    }

    .session-form__actions button:first-child:hover:not(:disabled) {
        background: var(--accent-hover);
    }

    .entry__body > .panel > .session-form__actions .link-button {
        display: none;
    }

    /*
        Search is docked to the foot of the screen: the field and its button stay in reach under
        the thumb while the shelf scrolls behind, and whatever the search has to say — results,
        the by-hand form, a notice — rises above the field and scrolls within the dock. Its own
        heading goes; the placeholder says what the box is for.
    */
    .search {
        /* The peek: the grip and the field, and nothing else — exactly tall enough to type in,
           whatever the screen. sheet.js reads this rather than carrying its own copy. */
        --sheet-peek: 132px;

        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 10;
        display: flex;
        flex-direction: column;
        /* The dock opens at its peek, and it is the stylesheet that puts it there: the sheet
           script sets the same height a beat later, so the first paint and the first frame the
           script owns are the same frame. (It used to open at half and drop to the peek once the
           script arrived.) overflow is hidden so a peeked sheet clips its results rather than
           spilling them up the screen. */
        height: var(--sheet-peek);
        overflow: hidden;
        /* No side padding: every horizontal strip of the dock belongs to something that answers a
           finger. The 1rem gutters used to be the dock's own, and a swipe begun in one of them —
           a thumb's width from either edge — found nothing to scroll here and scrolled the shelf
           behind instead. The gutters are the children's now. */
        padding: 0 0 calc(0.9rem + env(safe-area-inset-bottom));
        /* Nothing that starts inside the dock may end up moving the page under it. The results
           carry this too; this one is for everything that is not them — the foot below the field,
           the safe-area strip — where a scroll has nowhere of its own to go and would otherwise
           be handed straight to the shelf. */
        overscroll-behavior: contain;
        border-width: 1px 0 0;
        border-radius: 22px 22px 0 0;
        /* The tint for browsers with no backdrop filter, where the dock cannot be glass and has to
           be nearly paper: the shelf scrolls under it and results have to stay readable over it.
           Where the filter works, the @supports below makes it the same glass as the menus. */
        background-color: var(--card-raised);
        box-shadow: 0 -10px 30px rgb(0 0 0 / 12%);
        /* Only the height eases. bottom is how far the keyboard has lifted it (sheet.js), and
           that has to move with the keyboard, not glide after it: eased, every correction to
           it became a visible swoop. */
        transition: height 260ms var(--ease-out);
    }

    /* The same frosted glass as the account menu and the status sheet — sheerer, and blurred less,
       so the shelf under the dock stays there as shapes rather than dissolving into flat colour.
       The dock was the one surface still on the heavier tint and the longer blur, and it read as
       a panel where the other two read as glass. */
    @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
        .search {
            background-color: var(--card-frosted);
            -webkit-backdrop-filter: var(--blur-soft);
            backdrop-filter: var(--blur-soft);
        }
    }

    /*
        The handle you drag the sheet by: the whole strip across its top, and the only surface that
        resizes it, so a drag begun on the results scrolls them instead.

        It is the strip and not the pill. The pill is 2.5rem wide and the drag target was the pill:
        a finger that landed beside it — which is most of the top of the dock — was on the dock
        itself, which scrolls nothing, and the shelf behind took the swipe. The pill still draws
        where it did (::before, centred); what changed is how much of the top answers to it.
    */
    .search__grip {
        /* A flex row so the pill is centred by the box rather than by a button's own idea of how
           to place what is inside it, now that the box is wider than the pill. */
        display: flex;
        justify-content: center;
        align-items: flex-start;
        flex: 0 0 auto;
        align-self: stretch;
        width: auto;
        height: 1.5rem;
        margin: 0;
        padding: 0;
        background: transparent;
        /* Only the keyboard focus ring ever draws this box; it follows the dock's own top corners. */
        border-radius: 22px 22px 0 0;
        cursor: grab;
        touch-action: none;
        transition: none;
    }

    /*
        It is a button, so every rule written for buttons reaches it: the hover fill, the pressed
        squash, the fill a touch screen leaves behind after a tap. All of them turn the pill into
        a lozenge the moment a finger lands on it. The pill is the whole of what a reader sees
        of this control, so under a finger it looks exactly as it does at rest. The focus ring
        stays, for whoever reaches it by keyboard: focus-visible is not set by a tap.
    */
    .search__grip:hover:not(:disabled),
    .search__grip:active:not(:disabled) {
        background: transparent;
        transform: none;
    }

    .search__grip:active:not(:disabled) {
        cursor: grabbing;
    }

    .search__grip::before {
        content: "";
        display: block;
        flex: 0 0 auto;
        width: 2.5rem;
        height: 4px;
        margin-top: 0.6rem;
        border-radius: 999px;
        background: var(--line-strong);
    }

    /*
        Behind a button until it is wanted. The shelf is what a phone opens to; the dock across
        the foot of it, always there, was a third of the screen given to a thing done now and
        then. A round + at the bottom right, where the thumb already is, opens it and puts the
        cursor in the box; the ✕ in the box clears the search and closes it again.
    */
    .search:not(.search--open) {
        display: none;
    }

    .search--open {
        animation: dock 320ms var(--ease-out) backwards;
    }

    /* Closed, it drops back off the foot of the screen first (Home.razor, motion.js playOut). */
    .search--open.is-leaving {
        animation: undock 220ms cubic-bezier(0.4, 0, 1, 1) forwards;
    }

    /* Everything off the open dock is a tap that closes it, the same as its ✕. Clear, not dimmed:
       the shelf behind is narrowing as the reader types, and is worth seeing. */
    .search__scrim,
    .search__scrim:hover:not(:disabled) {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 9;
        padding: 0;
        border-radius: 0;
        background: transparent;
        cursor: default;
    }

    /* The dock's own Search button (and Enter) already ask the library; a second, larger button
       saying so took the room above the field that the results come up into. */
    .library-offer .button--large {
        display: none;
    }

    .library-offer:not(:has(p)) {
        display: none;
    }

    .search__fab {
        position: fixed;
        right: 1rem;
        bottom: calc(1rem + env(safe-area-inset-bottom));
        z-index: 10;
        display: grid;
        place-items: center;
        width: 3.4rem;
        height: 3.4rem;
        padding: 0;
        border-radius: 50%;
        background: var(--accent);
        color: var(--accent-ink);
        box-shadow: var(--shadow-lifted);
        animation: rise 320ms var(--ease-out) backwards;
    }

    .search__fab:hover:not(:disabled) {
        background: var(--accent-hover);
    }

    .search__fab svg {
        width: 1.4rem;
        height: 1.4rem;
    }

    /*
        Five rows of results at a time, then it scrolls: a result row is 0.6rem of padding either
        side of a 2.5rem-wide jacket at 2:3, so about 4.95rem, with 0.4rem between rows. More than
        five and the dock is the screen; fewer and a page of ten is a lot of thumbing.
    */
    .search__found {
        order: 0;
        flex: 1 1 auto;
        overflow-y: auto;
        min-height: 0;
        /* The dock's side gutters, carried by the scroller rather than by the dock, so a swipe
           that lands anywhere across the width of the results is a swipe on the results. */
        padding-inline: 1rem;
        overscroll-behavior: contain;
        /* Panning is the only gesture the results owe a finger. Stated, because a list that has
           not overflowed yet is a list with nothing to scroll, and the shelf behind is only too
           happy to take the swipe instead. */
        touch-action: pan-y;
    }

    /* A breath between the dock's top edge and the first result, and between the last and the box.
       Both are the scroller's own padding: as a margin, the lower gap was a strip of dock between
       the results and the field that scrolled the shelf behind when a thumb came down on it. */
    .search__found:not(:empty) {
        padding-top: 0.5rem;
        padding-bottom: 0.85rem;
    }

    .search-row {
        display: none;
    }

    .search__inputs {
        display: flex;
        order: 1;
        flex: 0 0 auto;
        margin: 0;
        /* The dock's gutters again — see .search__found. */
        padding-inline: 1rem;
    }

    /* Two-up is one-up in a dock this narrow. */
    .lib-grid {
        grid-template-columns: 1fr;
    }

    /* One row per result: a small jacket, a title that ellipsises, and a short button. */
    .result__body {
        min-width: 0;
    }

    .result__body strong {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    .result button {
        flex: 0 0 auto;
    }
}

/*
    On a phone the status list is a sheet from the foot of the screen, the way the search dock
    is, and the scrim dims what is behind it: a menu anchored to a card halfway down a scrolling
    shelf is easy to lose under a thumb.
*/
@media (max-width: 560px) {
    /* Stated for the hovered scrim too: the finger that opened the sheet is still over it. Above
       the search dock and its + (z-index 10) and the header (20): while a sheet is up, nothing
       behind it answers but the scrim, which closes it. */
    .status__scrim,
    .status__scrim:hover:not(:disabled) {
        z-index: 21;
        background: rgb(0 0 0 / 40%);
    }

    .status__menu {
        position: fixed;
        top: auto;
        right: 0;
        bottom: 0;
        left: 0;
        z-index: 22;
        min-width: 0;
        padding: 0.5rem 0.75rem calc(0.75rem + env(safe-area-inset-bottom));
        border: 0;
        border-radius: 18px 18px 0 0;
        animation: dock 260ms var(--ease-out) backwards;
    }

    /* The grip the dock has, so the sheet reads as the same kind of thing — though this one is
       not dragged, only dismissed. */
    .status__menu::before {
        content: "";
        display: block;
        width: 2.25rem;
        height: 4px;
        margin: 0 auto 0.5rem;
        border-radius: 2px;
        background: var(--line-strong);
    }

    .status__row {
        padding: 0.8rem 0.75rem;
        font-size: 1rem;
    }

    /*
        Logging reading is the same kind of sheet: up from the foot of the screen over the same
        scrim, with the same grip and the same slide, so the two things a card opens on a phone
        open the same way. The book is named at its head, since the sheet covers the card it
        came from, and it keeps its own Cancel: the Log reading button that would otherwise close
        it is under the scrim.
    */
    .sheet__scrim,
    .sheet__scrim:hover:not(:disabled) {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 21;
        padding: 0;
        border-radius: 0;
        background: rgb(0 0 0 / 40%);
        cursor: default;
        animation: fade 200ms var(--ease-out) backwards;
    }

    .sheet {
        position: fixed;
        right: 0;
        bottom: 0;
        left: 0;
        z-index: 22;
        max-height: 85dvh;
        margin: 0;
        overflow-y: auto;
        overscroll-behavior: contain;
        padding: 0.5rem 1rem calc(1rem + env(safe-area-inset-bottom));
        border-radius: 18px 18px 0 0;
        background: var(--card-frosted);
        -webkit-backdrop-filter: var(--blur-soft);
        backdrop-filter: var(--blur-soft);
        box-shadow: var(--shadow-lifted);
        animation: dock 260ms var(--ease-out) backwards;
    }

    /* A sheet scrolls already: the days unfolded in one are carried by it, not boxed again. */
    .sheet .days--scrolling,
    .sheet .days:has(> .day--more.is-leaving) {
        max-height: none;
        overflow-y: visible;
    }

    .sheet::before {
        content: "";
        display: block;
        width: 2.25rem;
        height: 4px;
        margin: 0 auto 0.75rem;
        border-radius: 2px;
        background: var(--line-strong);
    }

    .sheet__title {
        display: block;
        margin: 0 0 0.9rem;
        font-size: 1.05rem;
        font-weight: 650;
    }

    .sheet__book {
        display: block;
        margin-top: 0.1rem;
        overflow: hidden;
        font-size: 0.88rem;
        font-weight: 500;
        color: var(--ink-soft);
        white-space: nowrap;
        text-overflow: ellipsis;
    }

    .entry__body > .panel.sheet > .session-form__actions .link-button {
        display: inline-flex;
    }

    /*
        Settings is the same sheet, set as a phone's settings list: each setting is one row, its
        name at the left and its control at the right, with the hint in small type beneath and a
        rule between rows. Stacked and centred — label, then control, then hint — three settings
        took most of the screen to say three short things. Done is at the foot. Track by's list
        stays a list under its trigger here — the sheet is already the thing that came up from the
        foot, and a second sheet over it would hide the setting it is for.
    */
    .settings__grid {
        grid-template-columns: 1fr;
        gap: 0;
        padding: 0;
    }

    .settings__item {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas:
            "label control"
            "hint  hint";
        align-items: center;
        gap: 0.3rem 1rem;
        padding: 0.7rem 0;
        text-align: left;
    }

    .settings__item + .settings__item {
        border-top: 1px solid var(--line);
    }

    .settings__label {
        grid-area: label;
        font-size: 0.95rem;
        font-weight: 550;
        letter-spacing: 0;
        text-transform: none;
        color: var(--ink);
    }

    .settings__item > :nth-child(2) {
        grid-area: control;
        justify-self: end;
    }

    .settings__item .hint {
        grid-area: hint;
        max-width: none;
        text-wrap: pretty;
    }

    .settings__pages input {
        width: 4.5rem;
    }

    /* The sheet's title sits closer: the rows under it carry their own space. */
    .settings > .sheet__title {
        margin-bottom: 0.3rem;
    }

    .settings .status__scrim,
    .settings .status__scrim:hover:not(:disabled) {
        background: transparent;
    }

    .settings .status__menu {
        position: absolute;
        top: calc(100% + 0.35rem);
        right: 0;
        bottom: auto;
        left: auto;
        margin-left: 0;
        padding: 0.35rem;
        border: 1px solid var(--line);
        border-radius: var(--radius-sm);
        /* Solid: glass inside the sheet's glass has nothing left to blur, and the settings
           beneath read straight through it. */
        background: var(--card);
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
        animation: unfold 240ms var(--ease-out) backwards;
    }

    .settings .status__menu::before {
        display: none;
    }

    .sheet__foot {
        display: block;
        text-align: center;
        padding-top: 0.4rem;
    }

    /* Done is a word in the accent, as Log reading is on the card: the way on, without the
       weight of a filled button at the foot of a sheet that is mostly settings. */
    .sheet__foot .link-button {
        display: inline-flex;
        padding-inline: 1.5rem;
        color: var(--accent);
        font-weight: 600;
    }

    .sheet__foot .link-button:hover:not(:disabled) {
        background: var(--accent-soft);
        color: var(--accent);
    }

    /*
        Finding an edition is the shelf search's kind of sheet: its grip drags it between a peek
        (the field and no more), a half and nearly full, and taps between the peek and the half
        (js/sheet.js). The field is at its foot, under the thumb, and the results scroll above it.
        It opens at the half; sheet.js owns the height from then on, and the transition is what
        makes its snaps glide.
    */
    .panel.sheet.settings--finding {
        /* The grip, the heading, the field and its note: 24 + 40 + 54 + 24 + 16 of padding. */
        --sheet-peek: 158px;

        display: flex;
        flex-direction: column;
        height: 55dvh;
        max-height: none;
        overflow: hidden;
        padding-top: 0;
        transition: height 260ms var(--ease-out);
    }

    /* The search's own grip stands in for the sheet's. */
    .panel.sheet.settings--finding::before {
        display: none;
    }

    /* A hint's basis is the width of a row; in this column it would be the height of one. */
    .settings--finding > * {
        flex: 0 0 auto;
    }

    .settings--finding > .search__grip {
        display: flex;
        order: -2;
        margin-inline: -1rem;
    }

    /* One line at the head: the way back on the left, what this is in the middle. */
    .settings--finding > .sheet__title {
        order: -1;
        margin: 0 0 0.5rem;
        line-height: 2rem;
    }

    .settings--finding .sheet__book {
        display: none;
    }

    .settings--finding > .settings__back {
        position: absolute;
        top: 1.5rem;
        left: 0.5rem;
    }

    .settings--finding > .settings__results {
        order: 1;
        flex: 1 1 auto;
        min-height: 0;
        max-height: none;
        margin: 0 -0.25rem;
        touch-action: pan-y;
    }

    .settings--finding .settings__results:empty {
        display: block;
    }

    /* Peeked, the sheet is the field; a strip of results squeezed above it is only noise. */
    .settings--finding[data-sheet="peek"] > .settings__results {
        visibility: hidden;
    }

    .settings--finding > .settings__search {
        order: 2;
        padding-top: 0.6rem;
    }

    .settings--finding > .settings__note {
        order: 3;
    }

    /*
        Nothing behind a sheet moves either. A finger on the scrim would otherwise scroll the
        page under it, since the scrim has nothing of its own to scroll.
    */
    .sheet__scrim,
    .status__scrim {
        touch-action: none;
    }

    html:has(.sheet__scrim:not(.is-leaving), .status__menu:not(.is-leaving)) {
        overflow: hidden;
    }

    /* The search's + sits under either sheet at the same corner as its last button, and its glow
       showed through the frosted sheet behind it. It goes while a sheet is up. */
    body:has(.sheet, .status__menu) .search__fab {
        visibility: hidden;
    }

    /* Sheets, and the status list that is one here, drop back off the foot of the screen, and
       their scrim clears as they go. Track by's list inside Settings is a dropdown still, and
       lifts away like one. */
    /* A sheet comes up from the foot rather than growing in place. */
    .sheet.is-entering {
        overflow-y: auto;
        animation: dock 260ms var(--ease-out) backwards;
    }

    .sheet.is-leaving,
    .status__menu.is-leaving {
        overflow: hidden;
        animation: undock 220ms cubic-bezier(0.4, 0, 1, 1) forwards;
    }

    .settings .status__menu.is-leaving {
        animation: lift 160ms var(--ease) forwards;
    }

    .sheet__scrim.is-leaving,
    .sheet__scrim.is-leaving:hover:not(:disabled),
    .status__scrim.is-leaving,
    .status__scrim.is-leaving:hover:not(:disabled) {
        animation: clear 220ms cubic-bezier(0.4, 0, 1, 1) forwards;
    }
}

/* Shelf cover flow -------------------------------------------------------- */

/*
    The shelf on a phone held on its side (Components/ShelfDeck.razor): the welcome screen's cover
    flow (FlowStage, and the .flow rules above) with the shelf's jackets in it and nothing else on
    the screen, so the jackets can be as big as its little height allows. Only a phone on its side
    is given one (readingTracker.isSideways), so none of this is behind a media query.
*/

/* The screen to itself. The header and the heading are not drawn at all (MainLayout, Home); the
   search dock is kept, out of sight, so that turned upright the phone has it as it was left. */
main:has(.flow--shelf) :is(.search, .search__fab, .search__scrim) {
    display: none;
}

main:has(.flow--shelf) {
    --page-top: 0.75rem;

    padding-block: var(--page-top) 0;
}

.flow--shelf {
    /* A finger, not a cursor: nothing is picked up, so no room is kept above for it. */
    --pickup: 1;
    /* Less floor than the welcome's: its reflection has faded well before then, and the height is
       better spent on the jackets. */
    --floor: 0.5;
    /* Under the stage, for the caption's last line. */
    --caption-room: 1.9rem;
    /*
        As big as the height allows: the screen, less the page's padding above, the caption's room
        and a breath at the foot, is the stage, which is this many jacket widths tall (see
        .flow__stage and --headroom above).
    */
    --cover-w: clamp(
        4.5rem,
        (100dvh - var(--page-top) - var(--caption-room) - 0.75rem) / (1.5 + var(--floor) + 1.5 * (var(--pickup) - 1) + 0.03),
        15rem);

    margin-top: 0;
    padding-bottom: var(--caption-room);
}

/* What is left of the reflection is clipped rather than hanging below the stage, where it would
   make the page scroll. */
.flow--shelf .flow__stage {
    overflow: clip;
}

/*
    The book's spine, down its left side, where the back cover wraps round to meet the front: the
    colour of the cover, as a real book's is, and thicker than the welcome's dark strip, which it
    takes the place of. The open side keeps the welcome's page block. It is the cover's first
    sliver drawn out across the spine (--art, from ShelfDeck), or a drawn jacket's own board,
    shaded round, as a spine is: lit along its middle and darker where it turns into each cover.

    The photograph is drawn forty times the spine's width and anchored left, so the spine shows
    its first fortieth. That is the file's own edge: the jacket shows the cover cropped to fit
    (object-fit: cover), so on a cover of another shape it is a little way past the edge seen.
*/
.flow--shelf .cover {
    --spine-w: calc(var(--cover-w) * 0.06);

    border-left-width: 0;
    /* A drawn jacket's title and author clear the spine by as much as they cleared the strip. */
    padding-left: calc(var(--spine-w) + 0.85rem);
}

.flow--shelf .cover::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 1;
    width: var(--spine-w);
    background-color: inherit;
    background-image:
        linear-gradient(to right, rgb(0 0 0 / 34%), rgb(255 255 255 / 12%) 35%, rgb(0 0 0 / 8%) 70%, rgb(0 0 0 / 40%)),
        var(--art, none);
    background-size: 100% 100%, 4000% 100%;
    background-position: 0 0, left center;
}

.flow--shelf .flow__caption {
    bottom: 0;
    padding-inline: 3rem;
}

.flow--shelf .flow__caption strong,
.flow--shelf .flow__caption span {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.flow--shelf .flow__caption strong {
    font-size: 1.05rem;
}

/*
    Dealt out from the middle as the flow arrives — the phone turned on its side, the shelf come
    back to from a book, the ghosts giving way to the shelf — the nearest first. Only while the
    component says it is dealing: a jacket that comes into the window later, as the flow turns,
    turns in from the side with the rest.

    Done with the translate and scale properties, which sit on top of each jacket's own transform
    rather than replacing it: every jacket starts where the front one stands and slides out to its
    own place, already turned.
*/
@keyframes deal {
    from {
        opacity: 0;
        translate: calc(-1 * var(--x) * var(--cover-w)) 0.75rem;
        scale: 0.82;
    }
}

.flow--dealing .flow__slot {
    animation: deal 560ms var(--ease-out) backwards;
    animation-delay: calc(var(--depth) * 18ms);
}

/* The loading ghosts: the flow's shape in blank jackets, quieter than a shelf. */
.flow--pending .flow__stage {
    pointer-events: none;
}

.flow--pending .cover {
    filter: saturate(0.4);
    opacity: 0.5;
}

.flow--shelf > .empty {
    margin: 2.5rem 1rem 1rem;
    text-align: center;
}

/* The book at the front leaving for its own page: its jacket grows into the page's cover
   (readingTracker.openBook), a little slower than the page fades so the eye follows it. */
::view-transition-group(book-cover) {
    animation-duration: 420ms;
    animation-timing-function: var(--ease-out);
}
