/* Layout particular to athelney: the shelf, the copy form, the household list. */

.page-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  margin-bottom: 1rem;
}
.page-head h1 { margin: 0; }

.field { margin-bottom: 1rem; flex: 1; }
.row { display: flex; gap: 1rem; flex-wrap: wrap; }
.row .field { min-width: 11rem; }
/* A replacement value is a few digits: at full width its box would be a line
   of empty space. As wide as a sum of money, and no wider. */
.field-value { max-width: 18rem; }
.actions { display: flex; gap: 0.6rem; align-items: center; }

.search { margin-bottom: 1.75rem; }
.search-row { display: flex; gap: 0.5rem; align-items: center; }
.search-row input { flex: 1; }
.search .clear { font-size: 0.85rem; color: var(--text-faint); }

/* Whose books to show. A fieldset because the three are one choice, with the
   legend hidden visually but present for a screen reader — the radios alone
   would otherwise be three unlabelled options. */
.scope { border: 0; padding: 0.6rem 0 0; margin: 0; display: flex; gap: 1.1rem; flex-wrap: wrap; }
.scope-option {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin: 0;
  font-size: 0.87rem;
  color: var(--text-muted);
  cursor: pointer;
}
.scope-option input { accent-color: var(--accent); margin: 0; }
.scope-option:hover { color: var(--text); }

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

.book-list { list-style: none; padding: 0; margin: 0; }
.book-list li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.7rem 0;
  border-bottom: 1px solid var(--border);
}
.book-list li:last-child { border-bottom: none; }
.book-link { display: block; color: var(--text); }
.book-link:hover { color: var(--accent); text-decoration: none; }
.book-link:hover .book-title { text-decoration: underline; }
.book-title { display: flex; align-items: baseline; gap: 0.45rem; font-size: 1.05rem; }

/* Mine vs somebody else's, before the title. Two hues AND two shapes: colour
   alone would not survive a greyscale print or a red-green eye. */
.owner-icon { flex: none; display: inline-flex; align-items: center; align-self: center; }
.owner-icon.is-mine { color: var(--accent); }
.owner-icon.is-theirs { color: var(--text-faint); }
/* Between the two: a church copy is not yours, but it did not come from a
   stranger either, so it sits nearer the text than a direct share does. */
.owner-icon.is-church { color: var(--text-muted); }
h1 .owner-icon { margin-right: 0.5rem; }
h1 { display: flex; align-items: center; gap: 0.1rem; }
.book-meta { display: block; color: var(--text-muted); font-size: 0.87rem; }

.notes { white-space: pre-wrap; margin-top: 1.25rem; }

.notice {
  color: var(--text-muted);
  font-size: 0.9rem;
  border-left: 3px solid var(--border);
  padding: 0.35rem 0 0.35rem 0.75rem;
  margin: 0.75rem 0 1.25rem;
}
.book-meta.published { color: var(--accent); }

.check { margin-top: 0.5rem; }
.check-label {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 1rem;
  color: var(--text);
  cursor: pointer;
  margin: 0;
}
.check-label input { accent-color: var(--accent); margin: 0; }
.section { margin-top: 2.25rem; border-top: 1px solid var(--border); padding-top: 1.25rem; }
.danger { margin-top: 2.5rem; }

/* Who a book reaches, and the picker that changes it. */
.share-list { list-style: none; padding: 0; margin: 0 0 1rem; }
.share-list li {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.45rem 0;
  border-bottom: 1px solid var(--border);
}
.share-list li:last-child { border-bottom: none; }
.share-name { flex: 1; }
.share-list form { margin: 0; }

/* What a share shows, above the picker, as on Add a book's Share with. */
.share-grants { margin: 0 0 0.6rem; }
.share-form { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.share-form select {
  flex: 1;
  min-width: 12rem;
  padding: 0.45rem 0.6rem;
  font: inherit;
  font-size: 1rem;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.share-form select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--focus-ring);
}

/* --- names of unbounded length -------------------------------------------

   A library's name and a book's title are typed by somebody else and can be
   any length, including one unbroken word. Everywhere one lands, it needs two
   things: permission to shrink inside its flex parent (`min-width: 0`, since
   the default `min-width: auto` is a flex item refusing to go below its
   longest word) and permission to break that word rather than overflow.

   ONE rule, because this is one fact about one kind of content. Order I found
   it in four places at three different sizes of wrong: `.library-name` pushed
   the document to 695px, `.book-title` to 576px, and a book page's `h1` to
   1102px — each of them dragging the whole page sideways on a phone, which is
   the bug the payments table had by another road.

   `anywhere` and not `break-word`: only `anywhere` is counted when the browser
   works out a flex item's minimum width, and that minimum is the whole
   question here. It has a cost, and the block below pays it.

   SCOPED TO THE LIST, not to `.library-name` on its own. That class is also
   the account's name in the nav's account menu, and applying this there let
   the summary shrink below its content: the company's nav went from one line
   to "demo-" / "owner" with the chevron pushed under it. Caught in the browser
   one screenshot after the fix it belongs to. A rule about unbounded content
   has no business on a nav entry whose content is a single account name that
   the nav is sized for. */
.library-list .library-name,
.share-name,
.book-title,
main h1 {
  min-width: 0;
  overflow-wrap: anywhere;
}

.library-list { list-style: none; padding: 0; margin: 0; }
.library-list li {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.55rem 0;
  border-bottom: 1px solid var(--border);
  /* A flex item will not shrink below its content's minimum width unless it is
     told it may, and the default `min-width: auto` is exactly that refusal. A
     library whose name is one long unbroken word therefore pushed the row —
     and with it the whole document — off the right of a phone, the same bug
     the ledger had by another road. Found by stress-testing this list with a
     57-character name, not by reading it. */
  min-width: 0;
}
.library-list li:last-child { border-bottom: none; }
.library-name { font-weight: 600; }
.library-list .library-name { flex: 1; font-weight: 400; }
.badge {
  font-size: 0.75rem;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.05rem 0.5rem;
}
.library-list form { margin: 0; }

/* A whole row is the label, so the library name is part of the hit target and
   not just decoration sitting beside a 13px box. */
.paid-option {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex: 1;
  margin: 0;
  cursor: pointer;
  /* Same refusal as `.library-list li` above, one level in: on a church's own
     page the name sits inside this label, so both have to be allowed to shrink
     or the seat rows push the page sideways. */
  min-width: 0;
}
.paid-option input { accent-color: var(--accent); margin: 0; }
/* On the accounts page the box rides at the end of the row beside the badges,
   rather than taking the row the way it does on a church's own page. */
.paid-option-trailing { flex: 0 0 auto; font-size: 0.75rem; color: var(--text-muted); }
/* The fee box is its own form so it can save itself; without this it would
   claim a flex row of its own beside the Delete form. */
.fee-form { display: inline-flex; align-items: center; gap: 0.5rem; margin: 0; }
/* A reported standing, not a control: it sits where a button would and is
   deliberately quieter than one. */
.standing { flex: 0 0 auto; font-size: 0.8rem; }
.standing.is-paid { color: var(--accent); }
.standing.is-unpaid { color: var(--text-faint); }

@media (max-width: 34rem) {
  .row { flex-direction: column; gap: 0; }
  .page-head { flex-direction: column; align-items: flex-start; }
  /* Daniel: "on mobile (as per screen size threshold) provide no sign up
     button at all". Sign IN stays — the splash's job is still to open for
     somebody who already has a library.

     THIS HIDES SIGNING UP, IT DOES NOT PREVENT IT, and that is not a
     shortcoming of the rule but of what a rule can be. Athelney renders HTML
     with no JavaScript, so a viewport width never reaches the server and
     "screen size" can only ever be CSS. `/signup` stays reachable by address,
     a POST to it still works, and turning the phone sideways brings the link
     back. Daniel has read that and said hiding the link suffices for now.

     The threshold is this block's, not a second one of its own. Two
     nearly-equal numbers in one stylesheet are two numbers that drift. */
  .signup-link { display: none; }
  /* And the reverse: the rooms' Back to Pluteus (the Armarium, the
     Scriptorium) shows only here. Daniel: "only on mobile, when im on
     armarium forum or scriptorium, there also should be a back button which
     takes us back to pluteum". On a wider screen the nav's Pluteus is a click
     away. */
  .page-back.mobile-only { display: block; }
  /* Tap targets of about 44 px on a phone, measured in the no-breadcrumb order:
     the buttons were 41 px, and a Back link is one line of small text. The
     Back grows its box and gives the height back with negative margins, so it
     stands where it stood. .btn-small (the banner's Pay) keeps its size. */
  .btn:not(.btn-small) { padding-top: 0.55rem; padding-bottom: 0.55rem; }
  .page-back .back-link {
    display: inline-flex; align-items: center;
    min-height: 2.75rem; margin: -0.8rem 0; padding-right: 0.75rem;
  }
  /* And the invitation in the login page's body, which was the same offer in a
     louder place — "No library yet? Sign up — a household shelf is $60 a year,
     a church's is $500." The whole sentence, not the link inside it: the
     remainder with its middle cut out reads worse than either half.

     The login form is untouched. This is the invitation, not the door. */
  .signup-invite { display: none; }

  /* --- a list row stacks rather than squeezing its name ------------------

     THE WRAPPING IS THE SYMPTOM; THE WIDTH IS THE BUG. On /settings at 390px
     "Christ Church Omaha" rendered one or two letters per line while the
     `church` badge and `— contribution not paid` beside it sat comfortably on
     one. A browser only breaks mid-word when the box is narrower than the
     word, so the question was never how it wrapped but why its box was 103px
     tall and 103px wide.

     Measured rather than guessed. In a 342px row: `.standing` is
     `flex: 0 0 auto` at 154px and CANNOT shrink, `.badge` is 60px and has
     nothing to give, two gaps take 24px — so the name was handed the 104px
     left over and told to fit nineteen characters in it.

     And the reason it obeyed instead of overflowing is the rule above:
     `overflow-wrap: anywhere` drops a flex item's minimum width to one
     character, so the name absorbed ALL of the squeeze while two siblings
     absorbed none. That rule arrived in this same order, to stop long names
     pushing the document sideways. It fixed that and caused this.

     So: below this breakpoint the row stacks. The name gets the full width on
     its own line — at which point it wraps at spaces like ordinary prose, and
     `anywhere` only ever fires for a word genuinely wider than the screen —
     and the short things sit underneath it.

     NOT `.row`, which is a form helper for two fields side by side and whose
     `gap: 0` in this same block is right for fields with their own margins and
     wrong for a list row. Same technique, different element; making these
     `.row` would mean either breaking forms or overriding it straight back. */
  .library-list li,
  .share-list li {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.3rem;
  }
  /* Stop sharing is a list row's button, so .btn-small, and on a phone it is
     still a thing to tap: 44 px, measured in the share-while-adding order,
     where it was 30. The font stays small; only the box grows. A button
     centres its label by itself. */
  .share-list .btn-small { min-height: 2.75rem; }

  /* On a phone the camera control is the width of the phone: it is the thing
     tapped again and again with the other hand holding the book. */
  .photo-camera .btn-camera { flex: 1 1 100%; justify-content: center; }

  /* A shelf's mini-forms are compact where there is room and 44 px to tap on a
     phone, where forty of them are scrolled through with a thumb. */
  .shelf-book input[type=text], .shelf-book input[type=number],
  .shelf-location input { min-height: 2.75rem; }
  /* The × on a shelf photo's thumbnail keeps its 30 px circle and gets a
     44 px reach: the pseudo-element is part of the button, so a tap on it is
     a tap on ×. Only the shelf's, which is the one this order measured. */
  .shelf-photos .photo-drop::after { content: ""; position: absolute; inset: -0.45rem; }
  /* And its two other small things: the photo-library link, and the chips
     that point at the books a refused Save names. */
  .shelf-photos .photo-library { display: inline-flex; align-items: center; min-height: 2.75rem; }
  .shelf-problems .shelf-problem { min-height: 2.75rem; }

  /* The PDF viewer's dialog is the whole screen on a phone rather than a
     sheet on the page, and its page counter gives up its fixed width so the
     title keeps a little room. `[open]` and the extra class are specificity:
     this block comes before the viewer's own rules further down. */
  dialog.viewer[open] {
    width: 100vw; height: 100vh; height: 100dvh; margin: 0; border-radius: 0;
  }
  dialog.viewer .viewer-count { min-width: 0; }
  dialog.viewer .viewer-bar { gap: 0.35rem; }

  /* --- the nav: ☰ at the top left, a person at the far right --------------
     Daniel: "when we pass below threshold for mobile, we get this three
     stipes menu instead", and "name stays far right, maybe on mobile like a
     person in a circle". This block's threshold, not one of its own.

     The ☰ is shown, and the places hidden while closed, only where the
     browser has popovers. One that has not ignores `popover` and would keep a
     ☰ that opens nothing, so there the places simply stay in the row. */
  nav { align-items: center; padding-bottom: 0.5rem; }
  @supports selector(:popover-open) {
    nav .nav-toggle { display: inline-flex; }
    nav .nav-places:not(:popover-open) { display: none; }
  }
  nav .nav-toggle {
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    margin-left: -0.65rem;
    padding: 0;
    background: none;
    border: 0;
    border-radius: var(--radius);
    color: var(--text);
    cursor: pointer;
  }
  nav .nav-toggle:focus-visible,
  nav .nav-close:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
  /* Three stripes drawn from one bar and its two pseudo-elements, and
     aria-hidden: the same trick as the chevron, nothing to read out. */
  nav .stripes,
  nav .stripes::before,
  nav .stripes::after {
    display: block;
    width: 1.3rem;
    height: 2px;
    background: currentColor;
    border-radius: 1px;
  }
  nav .stripes { position: relative; }
  nav .stripes::before,
  nav .stripes::after { content: ""; position: absolute; left: 0; }
  nav .stripes::before { top: -7px; }
  nav .stripes::after { top: 7px; }
  /* The drawer, as sketched: the whole height, from the left, over the page,
     one place a row with a rule between. It is in the top layer while open,
     so it cannot widen the page and scroll it sideways. */
  nav .nav-places:popover-open {
    position: fixed;
    inset: 0 auto 0 0;
    width: min(82vw, 19rem);
    height: 100%;
    max-height: none;
    display: flex;
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 0;
    padding: 0.4rem 0 1rem;
    background: var(--surface);
    color: var(--text);
    border-right: 1px solid var(--border);
    box-shadow: 6px 0 24px rgba(0, 0, 0, 0.18);
    overflow-y: auto;
    font-size: 1.1rem;
  }
  nav .nav-places::backdrop { background: rgba(0, 0, 0, 0.35); }
  nav .nav-places:popover-open .nav-place {
    display: block;
    padding: 0.95rem 1.4rem;
    border-bottom: 1px solid var(--border);
    white-space: normal;
  }
  nav .nav-places:popover-open a.nav-place { color: var(--text); }
  nav .nav-places:popover-open .nav-place[aria-current="page"] { background: var(--body-bg); }
  /* The ✕ sits exactly where the ☰ was, which the drawer now covers, so the
     second tap of the order's "close on a second tap" lands on the same spot. */
  nav .nav-places:popover-open .nav-close {
    display: inline-flex;
    align-self: flex-start;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    margin: 1.1rem 0 0.4rem 0.85rem;
    padding: 0;
    background: none;
    border: 0;
    color: var(--text-muted);
    cursor: pointer;
  }
  nav .cross { position: relative; display: block; width: 1.15rem; height: 1.15rem; }
  nav .cross::before,
  nav .cross::after {
    content: "";
    position: absolute;
    left: 0;
    top: calc(50% - 1px);
    width: 100%;
    height: 2px;
    background: currentColor;
    border-radius: 1px;
  }
  nav .cross::before { transform: rotate(45deg); }
  nav .cross::after { transform: rotate(-45deg); }
  /* The account: the same <details>. Its name is kept for a screen reader,
     so it is still the summary's accessible name, but not drawn; the person
     is drawn instead, and the chevron goes. */
  nav .account-menu .account-name {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  nav .account-menu .chevron { display: none; }
  nav .account-menu .account-icon { display: block; }
  nav .account-menu > summary { padding: 0.5rem; margin-right: -0.5rem; border-radius: 50%; }
}

/* The sixty-day notice. Blue, and Daniel's choice: the account works, nothing
   is hidden and a date has passed — that is a notice, not a danger state, and
   colouring it as one would leave nothing louder for when something has
   actually gone wrong. It is also the only alert in this app, so it gets its
   own two variables rather than borrowing --danger at a lower opacity. */
.grace-banner {
  /* Full-bleed: the body is a centred reading column with padding, and these
     two lines take the bar back out to the viewport edge so it reads as a bar
     across the top rather than as a box inside the page. */
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-top: -1.5rem;
  margin-bottom: 1.5rem;
  padding: 0.7rem 1.5rem;
  background: var(--notice-bg);
  border-bottom: 1px solid var(--notice-border);
  color: var(--notice-text);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  flex-wrap: wrap;
  font-size: 0.92rem;
}
.grace-banner .btn {
  background: var(--notice-text);
  border-color: var(--notice-text);
  color: var(--notice-bg);
}
.grace-banner .btn:hover { opacity: 0.85; background: var(--notice-text); }

/* Picking what you are, and what you are buying. A radio with a body rather
   than a label, because each choice has a price and a sentence and the whole
   block should be the hit target. */
.choices { border: 0; padding: 0; margin: 0 0 1.5rem; display: block; }
.choice {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  margin: 0 0 0.75rem;
  padding: 0.8rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  font-size: 1rem;
  color: var(--text);
}
.choice:hover { border-color: var(--accent); }
.choice input { accent-color: var(--accent); margin: 0.35rem 0 0; flex: none; }
.choice-body { display: block; }
.choice-label { display: block; font-weight: 600; }
.choice-price { font-weight: 400; color: var(--accent); margin-left: 0.5rem; }
.choice-blurb { display: block; font-size: 0.87rem; color: var(--text-muted); }

/* Anything that takes money says so first. Quieter than an error and louder
   than a hint, because it is neither: it is the one fact about this screen
   somebody must not miss. */
.simulated {
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  padding: 0.6rem 1rem;
  margin: 1.25rem 0;
  font-size: 0.9rem;
  color: var(--text-muted);
}
.simulated p { margin: 0.4rem 0; }
.amount { font-size: 2rem; font-weight: 600; margin: 0.25rem 0; }

/* --- a wide thing scrolls, and the page does not --------------------------

   ONE class for every element too wide to fit a phone. Daniel, looking at the
   payments page on a narrow screen: "make that on mobile, the text stays where
   it is only the table is scrollable on the payments page" — his screenshot
   had the nav, the h1 and every paragraph slid off to the left, the heading
   reading "ents". A wide table with nothing around it drags the whole document
   sideways, and then the one thing on the page you cannot read is the prose
   telling you what you are looking at.

   `max-width: 100%` as well as `overflow-x`, because a block box does not
   shrink below its content without being told to — without it the wrapper is
   as wide as the table and scrolls nothing.

   THE SHADOWS ARE THE AFFORDANCE, and they cost four gradients and no script.
   Two solid ones painted in the page colour are `local`, so they travel with
   the content and cover the shadows when there is nothing more that way; two
   radial ones are `scroll`, so they stay pinned to the edges. The effect is a
   soft dark edge exactly where there is more to see and nowhere else. A region
   that scrolls with no sign that it can is a feature nobody finds — and on a
   touch screen there is no scrollbar to notice. */
.table-scroll {
  max-width: 100%;
  overflow-x: auto;
  background:
    linear-gradient(to right, var(--body-bg) 40%, rgba(0, 0, 0, 0)) left center / 2.5rem 100% no-repeat local,
    linear-gradient(to left, var(--body-bg) 40%, rgba(0, 0, 0, 0)) right center / 2.5rem 100% no-repeat local,
    radial-gradient(farthest-side at 0 50%, rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0)) left center / 0.65rem 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0)) right center / 0.65rem 100% no-repeat scroll;
}
/* It is focusable (see the handler's `tabindex`), so it needs to say so. */
.table-scroll:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* The receipt book. A table, because seven columns of facts is what a table
   is for, and this is the one page in athelney that is a record rather than
   a view of somebody's shelf.

   `width: 100%` and NOT `min-width: max-content`, which is what this tried
   first. Forcing every column to its single-line width makes the table 819px,
   and the page's own column is 688px — so the ledger scrolled on a **desktop**
   screen where it had always fitted. A fix for a phone that breaks a laptop is
   not a fix. Caught in the browser at 1400px, immediately after the 390px
   check said the mobile half was right.

   So the table lays out the way it always did: it fills the column when there
   is room and falls back to its minimum when there is not — at 390px that
   minimum is 469px against a 342px box, which is what the box is for. Nothing
   here shrinks the type or drops a column; the cells wrap, which is the
   browser doing its job. */
.ledger { width: 100%; border-collapse: collapse; font-size: 0.87rem; }
.ledger th, .ledger td {
  text-align: left;
  padding: 0.45rem 0.6rem 0.45rem 0;
  border-bottom: 1px solid var(--border);
  vertical-align: baseline;
}
.ledger th { color: var(--text-muted); font-weight: 600; font-size: 0.8rem; }
.ledger td.num { font-variant-numeric: tabular-nums; }
.tx-ref {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.82rem;
  color: var(--text-muted);
}

/* The `@media (max-width: 34rem)` rule that used to sit here shrank the
   ledger's type on a narrow screen. That was an attempt at this same bug and
   it did not work — the table still overflowed, it was merely smaller while
   doing it — and with `.table-scroll` around it the shrinking has nothing left
   to buy. Gone: the table now keeps one size at every width and scrolls
   inside its box. */

/* The front door, and the hub behind it.

   The splash is the one page centred in the screen, on both axes — Daniel:
   "on the splash page, the big athelny and the text below should be in the
   middle of the screen". Every signed-in page keeps the ordinary top-left
   layout; a front door is not a room.

   `min-height` and not `height`, and this is the whole of surviving a short
   viewport: the box is at least as tall as the screen under the nav, and grows
   past it when the content is taller, so the page scrolls normally instead of
   clipping or sliding under the nav. Checked at laptop height, not only in a
   tall window.

   The subtraction is the nav's own height plus the body's vertical padding —
   deliberately generous rather than exact, because being a little short of
   dead centre is invisible and being a little tall is a scrollbar on a page
   with two lines on it. */
.splash {
  min-height: calc(100vh - 9rem);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: 1rem 0 3rem;
}

/* The splash heading is bigger than every other h1 in the app, and that is the
   whole of its styling: it is the one page where the name of the thing is the
   page rather than a label on it. */
.splash-title { font-size: 3rem; margin-bottom: 0.75rem; }

/* One sentence, and a measure narrow enough to read. It is centred with the
   block: the two five-line paragraphs that used to sit here were left-aligned
   on purpose, because centred ragged text is hard to read at that length — and
   they are gone, so the reason is gone with them. Two lines centre fine. */
.splash-line {
  max-width: 32rem;
  margin: 0;
  font-size: 1.15rem;
  line-height: 1.55;
  color: var(--text-muted);
}

/* Pluteus, the dashboard: a greeting, one line counting what this library
   added in the last seven days, and the way to the shelf. The list of rooms
   it used to hold moved into the nav with the nav order, so `.hub-list` and
   `.hub-link` went with it. The "not yet" pill they had lives on as
   `.nav-soon` in base.css. */
.dashboard-count { font-size: 1.15rem; margin: 0.4rem 0 1.25rem; }

/* Add books: the heading, and the two modes right-aligned in its row. The row
   never stacks (unlike .page-head below 34rem): the heading wraps, and the two
   icons stay on the right. Each is a 44 px tap target. */
.title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.75rem; }
.title-row h1 { flex: 1 1 auto; min-width: 0; }
.mode-switch { display: flex; flex: none; gap: 0.3rem; margin-top: 0.2rem; }
.mode-link {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.75rem; height: 2.75rem;
  color: var(--text-muted); border: 1px solid var(--border); border-radius: var(--radius);
}
.mode-link:hover { color: var(--accent); border-color: var(--accent); text-decoration: none; }
.mode-link[aria-current="page"] {
  color: var(--accent); border-color: var(--accent); background: var(--surface);
  box-shadow: inset 0 -3px 0 var(--accent);
}
/* The shelf mode: its photo box, then the list of books a reading found,
   one mini-form each inside one form. */
.shelf-photos .actions { align-items: center; }
.shelf-list > h2 { font-size: 1.1rem; margin: 1.5rem 0 0.4rem; }
.shelf-location { max-width: 32rem; }
.shelf-book { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); padding: 0.4rem 0.8rem 0.3rem; margin: 0.8rem 0 0; min-width: 0; }
.shelf-book legend { font-size: 0.8rem; color: var(--text-muted); padding: 0 0.3rem; }
/* Unsure: the model could only partly read the spine. Said in the legend, so
   it is the first thing read about the book, and in the danger colour at a
   whisper: it asks for a look, it is not an error. */
.shelf-unsure { color: var(--danger); font-style: italic; }
.shelf-book.has-error { border-color: var(--danger); }
.shelf-book > .error { margin: 0.2rem 0 0.5rem; font-size: 0.9rem; }
/* A book not kept is set aside, not hidden: its fields fade and its border
   breaks, so a list with a few unticked reads at a glance. Browsers without
   :has() show every book alike, which is only less telling. */
.shelf-book:has(.shelf-keep input:not(:checked)) { border-style: dashed; background: transparent; }
.shelf-book:has(.shelf-keep input:not(:checked)) > .field,
.shelf-book:has(.shelf-keep input:not(:checked)) > .row { opacity: 0.55; }
.shelf-book .field { margin-bottom: 0.4rem; }
.shelf-more-end { margin-top: 1rem; }
/* Save's refusal over the list: its sentence, then the books it points at as
   chips, each a link to its mini-form. */
.shelf-list > .error { margin: 0.5rem 0 1.25rem; }
.shelf-list > .error p { margin: 0; }
.shelf-problems { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.5rem !important; }
.shelf-problem {
  display: inline-flex; align-items: center; min-height: 2.25rem; padding: 0 0.75rem;
  border: 1px solid var(--danger); border-radius: var(--radius); color: var(--danger);
}
.shelf-problem:hover { background: var(--danger); color: var(--body-bg); text-decoration: none; }
/* Author and year side by side even on a phone, where .row otherwise stacks:
   the list is meant to look compact, a list of small forms. */
.shelf-book .row { flex-direction: row; flex-wrap: nowrap; gap: 0.6rem; }
.shelf-book .row .field { min-width: 0; }
.shelf-book input { padding: 0.3rem 0.5rem; font-size: 0.95rem; }
.shelf-book label { margin-bottom: 0.1rem; }
.shelf-book .shelf-year { flex: 0 0 6rem; min-width: 5rem; }
/* The whole label is the tap target, 44 px tall, as a share row is. */
label.shelf-keep { display: inline-flex; align-items: center; gap: 0.6rem; min-height: 2.75rem; margin: 0; font-size: 0.95rem; color: var(--text); cursor: pointer; }
.shelf-keep input { flex: none; width: 1.15rem; height: 1.15rem; margin: 0; accent-color: var(--accent); }

/* From photos, above the fields on Add books. A box of its own, because it is
   a second form on the page and the Save below does not submit it. */
.from-photos, .share-box {
  margin: 0.5rem 0 1.75rem;
  padding: 0.9rem 1rem 0.2rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.from-photos h2, .share-box h2 { margin: 0 0 0.3rem; font-size: 1.1rem; }
/* One-book mode's box folds (a <details>): the whole summary row is the tap
   target, at least 44 px, with the account menu's drawn chevron turning when
   it opens. The box's own padding moves onto the summary so the row, not only
   the words, is what a thumb hits. Share with, lower on the same page, folds
   the same way and borrows every rule. */
details.from-photos, details.share-box { padding: 0 1rem 0.2rem; }
details.from-photos:not([open]), details.share-box:not([open]) { padding-bottom: 0; }
.from-photos-summary, .share-box-summary, .shelf-share-one-summary {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  min-height: 2.75rem; margin: 0 -1rem; padding: 0.35rem 1rem;
  cursor: pointer; list-style: none;
}
.from-photos-summary::-webkit-details-marker, .share-box-summary::-webkit-details-marker,
.shelf-share-one-summary::-webkit-details-marker { display: none; }
.from-photos-summary::marker, .share-box-summary::marker,
.shelf-share-one-summary::marker { content: ""; }
.from-photos-summary:focus-visible, .share-box-summary:focus-visible,
.shelf-share-one-summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; border-radius: var(--radius); }
.from-photos-title, .share-box-title { font-size: 1.1rem; font-weight: 600; }
.from-photos-summary .chevron, .share-box-summary .chevron, .shelf-share-one-summary .chevron {
  flex: none; width: 0.5em; height: 0.5em;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: translateY(-0.15em) rotate(45deg); transition: transform 0.12s ease;
}
details.from-photos[open] > .from-photos-summary .chevron,
details.share-box[open] > .share-box-summary .chevron,
details.shelf-share-one[open] > .shelf-share-one-summary .chevron { transform: translateY(0.1em) rotate(-135deg); }
details.from-photos[open] > .from-photos-summary,
details.share-box[open] > .share-box-summary { margin-bottom: 0.2rem; }
.from-photos .hint { margin: 0 0 0.8rem; }
.from-photos .notice,
.from-photos .error { margin: 0 0 0.8rem; }

/* Share with, inside the book form below Notes, and its two drafts in the
   shelf mode's preview. The box and its summary are From photos' (above).
   How many are ticked is a quieter part of the summary. */
.share-box { margin: 0.25rem 0 1.25rem; }
.share-count { font-weight: 400; color: var(--text-muted); }
.share-box .hint { margin: 0 0 0.5rem; }
/* The two groups, Churches and Home Libraries: a fieldset each, its legend
   the group's name. */
.share-group { border: 0; margin: 0 0 0.6rem; padding: 0; min-width: 0; }
.share-group legend { font-size: 0.85rem; color: var(--text-muted); padding: 0; margin-bottom: 0.1rem; }
/* One account per row on a phone, columns where there is room. Each row is
   the whole label, 44 px tall, so the name is as good a target as the box. */
.share-choices {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 13rem), 1fr));
  column-gap: 1rem;
}
label.share-choice {
  display: flex; align-items: center; gap: 0.6rem;
  min-height: 2.75rem; margin: 0;
  font-size: 0.95rem; color: var(--text); cursor: pointer;
}
.share-choice input { flex: none; width: 1.15rem; height: 1.15rem; margin: 0; accent-color: var(--accent); }
.share-choice span { min-width: 0; overflow-wrap: anywhere; }
/* The shelf's two: Share all of these with…, the same box, open, with the
   rule under it; and the per-book one folded small inside each mini-form. */
section.share-box.shelf-share-all { margin: 0.8rem 0 0; padding-bottom: 0.3rem; }
.share-rule { font-size: 0.87rem; color: var(--text-muted); margin: 0.2rem 0 0.6rem; }
.shelf-share-one { margin: 0 0 0.3rem; }
.shelf-share-one-summary { margin: 0; padding: 0.35rem 0; font-size: 0.85rem; color: var(--text-muted); justify-content: flex-start; }
.shelf-share-one .share-choices { grid-template-columns: repeat(auto-fill, minmax(min(100%, 11rem), 1fr)); }
.shelf-share-one label.share-choice { font-size: 0.85rem; }
.photo-form { margin: 0; }
.photo-form .photo-pick { margin-bottom: 0.8rem; }
/* Once the script has taken over, the plain input is still the one that is
   submitted — only out of sight, not `display: none`, so the label that opens
   the phone's photo library can still reach it. */
.photo-form.enhanced .photo-pick {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* `hidden` has to be restated wherever `display` is set, or it loses. */
.photo-camera[hidden], .photo-thumbs[hidden] { display: none; }
.photo-camera {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem 1rem;
  margin-bottom: 0.8rem;
}
.btn-camera {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  margin: 0;
  padding: 0.75rem 1.4rem;
  font-size: 1.1rem;
  color: var(--body-bg);
}
.photo-camera.full .btn-camera { opacity: 0.45; cursor: default; }
.photo-library {
  margin: 0;
  font-size: 0.9rem;
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 0.15em;
}
.photo-thumbs {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  margin: 0 0 0.9rem;
  padding: 0.45rem 0 0;
}
.photo-thumbs li { position: relative; }
.photo-thumbs img {
  display: block;
  width: 4.75rem;
  height: 4.75rem;
  object-fit: cover;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--body-bg);
}
/* Big enough for a thumb, and over the corner so it is plainly about the
   photo it sits on. */
.photo-drop {
  position: absolute;
  top: -0.55rem;
  right: -0.55rem;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
}
.photo-drop:hover { color: var(--danger); border-color: var(--danger); }
.photo-form .actions { flex-wrap: wrap; margin-bottom: 0.8rem; }
.photo-status { font-size: 0.85rem; color: var(--text-muted); }
.btn:disabled { opacity: 0.6; cursor: progress; }

/* PDFs on the shelf, and asking them. */
/* `flex: none`, or a long title squeezes the badge into a column of letters. */
.badge-pdf { flex: none; white-space: nowrap; align-self: center; }
.book-title .badge-pdf { font-size: 0.68rem; letter-spacing: 0.04em; }
h1 .badge-pdf { margin-left: 0.6rem; font-size: 0.75rem; font-weight: 400; letter-spacing: 0.04em; }
.page-head .actions { flex-wrap: wrap; }
.ask-link { margin: -0.4rem 0 1.25rem; font-size: 0.95rem; }
.lead { color: var(--text-muted); margin: 0 0 1.25rem; }
.notice.warn { border-left-color: var(--danger); }
.pdf-file { word-break: break-word; }
.busy-note { font-size: 0.85rem; color: var(--text-muted); margin: 0.6rem 0 0; }
.busy-note[hidden] { display: none; }
.ask-form textarea { min-height: 4.5rem; }
.answer { margin-top: 2rem; border-top: 1px solid var(--border); padding-top: 1.1rem; }
.answer h2 { margin: 0 0 0.6rem; font-size: 1.15rem; }
.answer h3 { margin: 1.5rem 0 0.4rem; font-size: 0.95rem; color: var(--text-muted); }
.answer .asked { color: var(--text-muted); font-size: 0.9rem; }
.answer p { line-height: 1.55; }
.cite { font-size: 0.8rem; white-space: nowrap; margin-left: 0.15rem; }
.sources { margin: 0; padding-left: 1.4rem; }
.sources li { margin-bottom: 0.35rem; }
.source-pages { color: var(--text-muted); font-size: 0.87rem; }
/* A page number that opens the viewer has to look like a link, small as it is. */
.source-pages a { text-decoration: underline; text-underline-offset: 0.15em; }

/* The PDF viewer. The dialog is the whole screen on a phone, a tall sheet on
   a desktop, and dark around the page either way, so the page reads as paper.
   The plain page without JavaScript is `.viewer-sheet` in the column with
   `.pager` above and below it. */
.page-head .actions .btn + .btn { margin-left: 0.2rem; }
.no-file { color: var(--text-muted); }
/* A <nav>, so it must shed the rule and the spacing the top nav has. */
nav.pager { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; margin: 0.6rem 0; padding: 0; border: 0; font-size: 0.95rem; }
/* Which PDF this is, and the link back to its page. It sat under a trail
   that named it too, until the breadcrumb went, so it stays modest. */
h1.viewer-heading { font-size: 1.25rem; line-height: 1.3; margin: 0; }
.pager-end { color: var(--text-faint); }
.pager-count { color: var(--text-muted); white-space: nowrap; }
p.viewer-sheet { margin: 0; }
p.viewer-sheet img { display: block; width: 100%; height: auto; background: #fff; border: 1px solid var(--border); }

html.viewer-open { overflow: hidden; }
dialog.viewer {
  padding: 0; border: 0;
  width: min(92vw, 62rem); max-width: none; height: 94vh; max-height: none; margin: auto;
  border-radius: 6px;
  background: #1c1d1f; color: #ecebe6;
}
dialog.viewer[open] { display: flex; flex-direction: column; }
dialog.viewer::backdrop { background: rgba(0, 0, 0, 0.7); }
.viewer-bar { display: flex; align-items: center; gap: 0.45rem; padding: 0.45rem 0.6rem; font-size: 0.9rem; }
.viewer-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.viewer-count { white-space: nowrap; min-width: 6.5rem; text-align: center; }
.viewer-bar button {
  min-width: 2.6rem; min-height: 2.6rem; padding: 0 0.5rem;
  background: transparent; color: inherit; font: inherit; font-size: 1.15rem; line-height: 1;
  border: 1px solid rgba(236, 235, 230, 0.3); border-radius: var(--radius); cursor: pointer;
}
.viewer-bar button:hover:not(:disabled) { border-color: rgba(236, 235, 230, 0.7); }
.viewer-bar button:disabled { opacity: 0.3; cursor: default; }
.viewer-bar button:focus-visible { outline: 2px solid #8fc0a3; outline-offset: 1px; }
/* margin: auto centres the page while it fits, and falls to 0 once it is
   bigger than the sheet, so every part of an enlarged page can be scrolled to;
   centring with justify-content would push its left edge out of reach. */
dialog.viewer .viewer-sheet { position: relative; flex: 1; min-height: 0; overflow: auto; display: flex; padding: 0 0.4rem 0.4rem; }
dialog.viewer .viewer-sheet img { display: block; max-width: 100%; max-height: 100%; width: auto; height: auto; margin: auto; background: #fff; cursor: zoom-in; transition: opacity 0.15s; }
dialog.viewer.zoomed .viewer-sheet img { max-width: none; max-height: none; cursor: zoom-out; }
dialog.viewer.loading .viewer-sheet img { opacity: 0.35; }
.viewer-status { position: absolute; left: 0; right: 0; top: 40%; margin: 0; text-align: center; font-size: 0.9rem; pointer-events: none; }
.viewer-status:empty { display: none; }
