/* Vivlio — a private library.
 *
 * Two ideas drive the look. First, a library is warm and printed, so the
 * surfaces are paper rather than grey chrome and the type is a reading serif.
 * Second, a wall of 3,000 covers is only beautiful if every tile is filled —
 * so books with no cover art get a generated cloth binding rather than a grey
 * rectangle, and the wall reads as a bookcase instead of a broken table.
 */

/* ---------- tokens ---------- */

:root {
  --paper:      #f8f1e8;
  --surface:    #fffcf8;
  --surface-2:  #f1e7da;
  --ink:        #241715;
  --ink-soft:   #55423d;
  /* 4.04:1 on paper, 3.71 on the second surface, 3.26 on the apricot column —
     and this is the colour nearly every explanatory sentence in the app is
     set in. It was below the legibility floor everywhere it was used, which
     is not a matter of taste and was never going to be fixed by arguing about
     pixels. Measured against every ground it lands on and darkened until the
     worst of them clears 4.5:1; see the check in test/server.mjs, which now
     computes these ratios rather than trusting anybody's eye. */
  --muted:      #6b5a52;
  --rule:       #e3d5c5;
  --rule-soft:  #efe5d8;
  --accent:     #9e1f32;
  --accent-ink: #fff8f0;
  --accent-lo:  #7d1626;
  --slate:      #195a56;
  --good:       #195a56;
  --warn:       #9d6118;
  --apricot:    #fed3a8;
  --on-apricot: #241715;
  /* The whole sidebar, not only its masthead. With the column down to four
     buttons and a paragraph it can carry the identity's own colour, and that
     is what separates it from the wall of books beside it — a rule alone was
     never going to. In the dark theme it is a deep ground instead: apricot at
     full strength down a whole column at night is a lamp, not a panel.
     `--on-panel` is what sits on top of it: the buttons and the cards give up
     the paper the column has taken. */
  --panel:      #fed3a8;
  --on-panel:   color-mix(in srgb, #fffcf8 72%, #fed3a8);
  --panel-rule: color-mix(in srgb, #9e1f32 22%, #fed3a8);
  /* The two halves of the name. They are tokens rather than fills baked into
     the file because the sidebar is apricot by day and a deep ground by
     night, and pine at full strength on that night ground very nearly
     disappears — the mark was there and unreadable, which is the worst of
     both. */
  --mark-pine:    #195a56;
  --mark-carmine: #9e1f32;

  --shadow-1: 0 1px 2px rgba(36,23,21,.06), 0 2px 10px rgba(36,23,21,.05);
  --shadow-2: 0 2px 8px rgba(36,23,21,.10), 0 14px 34px rgba(36,23,21,.12);
  --shadow-book: 0 1px 2px rgba(36,23,21,.18), 0 8px 18px rgba(36,23,21,.16);

  /* Alegreya is a book face with a real Greek cut — which most interface
     typefaces have not got, and four fifths of this library is Greek. Pattaya
     carries the name and nothing else: it has no Greek glyphs at all, so the
     first Greek title set in it would fall back silently to something else. */
  --serif: 'Alegreya', 'Georgia', 'Times New Roman', serif;
  --sans: 'Alegreya Sans', system-ui, -apple-system, 'Segoe UI', 'Helvetica Neue', sans-serif;
  --mono: ui-monospace, 'SF Mono', 'Menlo', monospace;
  /* The name's face. It has NO GREEK GLYPHS — not one — so it may only ever be
     applied to a Latin string that is part of the identity. Anything else set
     in it falls back silently and looks like a bug nobody can find. */
  /* No `cursive` at the end of this stack: on a machine with no Pattaya and no
     Alegreya that resolves to Comic Sans, which is a worse thing to see than a
     plain serif. */
  --logo: 'Pattaya', 'Alegreya', Georgia, serif;

  --r-sm: 4px;
  --r-md: 8px;
  --r-lg: 14px;

  --sidebar: 268px;
  /* One band across the whole window, sidebar and all — see `.brand` and
     `.topbar`.

     Derived, not chosen. The wordmark has to span the sidebar's column
     exactly — both edges level with the destination buttons under it, which
     is the one thing about the masthead that measurement can confirm and the
     eye cannot — so its width is fixed at 100% and its height follows from
     the viewBox's proportion. The bar beside it then has to be that tall plus
     its own breathing room, or the two rules do not meet.

     4.8461 is the mark's aspect ratio. If the wordmark is ever redrawn with a
     different viewBox this number goes stale, and two tests catch it: the
     browser suite measures the mark against the buttons below it, and again
     measures the two rules against each other. */
  --mark-h: calc((var(--sidebar) - 32px) / 4.8461);
  --topbar: calc(var(--mark-h) + 27px);
  --tabbar: 0px;

  /* The safe areas, via variables rather than env() at each use.
     Two reasons. It keeps 'the bar is 58px tall plus whatever the phone
     reserves' expressible as arithmetic instead of scattered guesses. And a
     test can set these to an iPhone's real values on a desktop browser, which
     makes a whole class of layout bug — the kind that only appears on a device
     with a home indicator — something that can be caught before you see it. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper:     #12191a;
    --surface:   #1a2324;
    --surface-2: #222d2e;
    --ink:       #f2e7db;
    --ink-soft:  #d5c7b8;
    --muted:     #9d8f82;
    --rule:      #2d3a3a;
    --rule-soft: #222c2d;
    --accent:    #e46d7b;
    --accent-ink:#12191a;
    --accent-lo: #c9525f;
    --slate:     #6fc0b0;
    --good:      #6fc0b0;
    --warn:      #e3a951;
    --apricot:   #fed3a8;
    --on-apricot:#241715;
    --panel:     #16201f;
    --on-panel:  #1f2b2a;
    --panel-rule:#2d3a3a;
    --mark-pine:    #7fc0a8;
    --mark-carmine: #e8807c;
    --shadow-1: 0 1px 2px rgba(0,0,0,.5);
    --shadow-2: 0 2px 8px rgba(0,0,0,.55), 0 14px 34px rgba(0,0,0,.5);
    --shadow-book: 0 1px 3px rgba(0,0,0,.7), 0 8px 20px rgba(0,0,0,.55);
  }
}

[data-theme="dark"] {
  --paper:   #12191a;
  --surface:   #1a2324;
  --surface-2: #222d2e;
  --ink:     #f2e7db;
  --ink-soft:  #d5c7b8;
  --muted:   #9d8f82;
  --rule:    #2d3a3a;
  --rule-soft: #222c2d;
  --accent:  #e46d7b;
  --accent-ink:#12191a;
  --accent-lo: #c9525f;
  --slate:   #6fc0b0;
  --good:    #6fc0b0;
  --warn:    #e3a951;
  --apricot:   #fed3a8;
  --on-apricot:#241715;
  --panel:     #16201f;
  --on-panel:  #1f2b2a;
  --panel-rule:#2d3a3a;
  --mark-pine:    #7fc0a8;
  --mark-carmine: #e8807c;
  --shadow-1: 0 1px 2px rgba(0,0,0,.5);
  --shadow-2: 0 2px 8px rgba(0,0,0,.55), 0 14px 34px rgba(0,0,0,.5);
  --shadow-book: 0 1px 3px rgba(0,0,0,.7), 0 8px 20px rgba(0,0,0,.55);
}

/* ---------- base ---------- */

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  overscroll-behavior-y: none;
}

body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  /* Raised with everything else. The default was never the problem — almost
     nothing used it, because the app's own classes sat three and four pixels
     below it and those are what every sentence is set in. */
  font-size: 16.5px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-feature-settings: 'kern' 1, 'liga' 1;
}

h1, h2, h3, h4 { font-family: var(--serif); font-weight: 700; line-height: 1.16;
  margin: 0; letter-spacing: -.012em; text-wrap: balance; }

/* `font: inherit` is not enough for a search field: the platform reasserts its
   own face through -webkit-appearance, which is why the search box looked like
   a different typeface from everything beside it. */
button, input, select, textarea { font: inherit; color: inherit; font-family: var(--sans); }
input[type="search"] { appearance: none; -webkit-appearance: none; }
/* A menu is a control, not a field.
 *
 * It was briefly swept into the rule above, which is for things you type
 * into — and the result was that «Title» and «Medium» in the toolbar grew to
 * the size of text boxes and lost their arrows, because the enlarged padding
 * overrode the `padding-right` that leaves room for them. A select is read
 * and picked from; it belongs with the buttons it stands beside. */
select {
  appearance: none; -webkit-appearance: none;
  background-color: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  font: inherit; font-size: 15px;
  color: var(--ink);
  padding: 7px 30px 7px 13px;
  background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 15px) 52%, calc(100% - 10px) 52%;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
}

button {
  cursor: pointer;
  border: 1px solid var(--rule);
  background: var(--surface);
  border-radius: var(--r-md);
  padding: 7px 13px;
  transition: background .12s ease, border-color .12s ease, transform .06s ease;
}
button:hover { background: var(--surface-2); }
button:active { transform: translateY(1px); }
button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* `hidden` means hidden, whatever else the element's class says.
 *
 * The attribute's own rule is the browser's weakest — a single class with a
 * `display` of its own beats it — so every element here that is put away with
 * `el.hidden = true` and also laid out with flex was quietly never put away.
 * It had happened three times (the sign-in card, the stats rows, and the list
 * rename in v55) and been patched three times, one selector each. Once, here,
 * for all of them. */
[hidden] { display: none !important; }

button.primary {
  background: var(--accent); color: var(--accent-ink); border-color: transparent; font-weight: 550;
}
button.primary:hover { background: var(--accent-lo); }
button.ghost { border-color: transparent; background: transparent; }
button.ghost:hover { background: var(--surface-2); }
button[disabled] { opacity: .45; pointer-events: none; }

/* Every field you can type into, in one rule.
 *
 * `email` was missing from this list, which is not a small omission: it meant
 * the sign-in field, the invite box and the change-of-address box were bare
 * browser defaults — small, square, and set in whatever the browser fancied —
 * on the three screens where somebody meets the app for the first time. A
 * selector list is a list somebody has to remember to add to, and this is
 * what forgetting looks like.
 *
 * Sized for the longest thing anyone types into it. An email address is the
 * hardest case, and a field that scrolls while you type it is a field you
 * cannot proof-read before pressing the button. */
input[type="text"], input[type="search"], input[type="url"], input[type="password"],
input[type="number"], input[type="email"], input[type="tel"], input[type="date"],
input:not([type]), textarea {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  padding: 11px 14px;
  font-size: 16px;
  font-family: inherit;
  color: var(--ink);
  width: 100%;
}
textarea { resize: vertical; min-height: 74px; font-family: inherit; }

/* A file field, in the app's own language.
 *
 * Styling the button rather than replacing the control: the native input is
 * what the operating system's file picker is attached to, and hiding it
 * behind a label of our own trades a plain appearance for a class of bug
 * nobody enjoys. `::file-selector-button` is the supported way to say this. */
input[type="file"] {
  background: var(--surface);
  border: 1px dashed var(--rule);
  border-radius: var(--r-lg);
  padding: 12px 14px;
  font-size: 14px;
  color: var(--ink-soft);
  width: 100%;
}
input[type="file"]::file-selector-button {
  cursor: pointer;
  border: 1px solid transparent;
  background: var(--accent);
  color: var(--accent-ink);
  font: inherit;
  font-weight: 550;
  border-radius: var(--r-md);
  padding: 7px 13px;
  margin-right: 12px;
  transition: background .12s ease;
}
input[type="file"]::file-selector-button:hover { background: var(--accent-lo); }
label { display: block; font-size: 14.5px; font-weight: 550; color: var(--ink-soft); margin-bottom: 5px; }

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }

.hidden { display: none !important; }
.mono { font-family: var(--mono); font-size: .9em; }
.muted { color: var(--muted); }
/* The size most of this app is actually read at, which is why it is no
   longer three pixels under the body default. */
.small { font-size: 15px; }

::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-thumb { background: var(--rule); border-radius: 6px; border: 3px solid var(--paper); }
::-webkit-scrollbar-track { background: transparent; }

/* ---------- app frame ---------- */

.app { display: flex; min-height: 100%; }

/* Two grounds in the whole app, not three.
 *
 * The sidebar used to be --surface, the library --paper and the masthead
 * apricot, which reads as three decisions where there is only one idea. The
 * sidebar and the library are now the same paper, separated by one rule that
 * runs the full height; --surface is kept for things that genuinely sit ON the
 * page — cards, inputs, the book sheet. Apricot stays the one exception,
 * because it is the app speaking rather than a surface. */
.sidebar {
  width: var(--sidebar);
  flex: 0 0 var(--sidebar);
  border-right: 1px solid var(--panel-rule);
  background: var(--panel);
  padding: 20px 16px 28px;
  position: sticky; top: 0;
  height: 100vh;
  /* Scrolls, but never draws a bar.
     Library has more facets than fit and Settings has fewer, so one page had a
     scrollbar and the other did not — and because a bar takes width from the
     column, the masthead, the nav markers and the facet rules all shifted
     sideways on the way between them. Reserving a permanent gutter instead
     would have been worse: everything in this column bleeds to its edges, and
     a reserved gutter puts a stripe of paper down the right of the apricot
     band. So the bar is simply not drawn. The wheel, the trackpad and a finger
     all still scroll, and both pages look the same. */
  overflow-y: auto;
  scrollbar-width: none;
  display: flex; flex-direction: column; gap: 20px;
}
.sidebar::-webkit-scrollbar { width: 0; height: 0; }

/* Nothing in this column is allowed to be squashed.
 *
 * This is the whole of the masthead bug, and it is worth naming because the
 * shape of it will happen again. A flex item's default is `flex-shrink: 1`,
 * so when the column's contents are taller than the column — which on a phone
 * is *usually*, and in landscape is always — the browser takes the missing
 * height out of the items rather than scrolling. The masthead lost 78 of its
 * 114 pixels that way. Its box shrank; the wordmark inside it did not, because
 * the wordmark's height comes from its own width; and the carmine rule, which
 * is the masthead's bottom border and is therefore drawn at the bottom of
 * whatever the box has been reduced to, ended up lying across the middle of
 * the name.
 *
 * Reported as "the separator bar falls on top of the logo", which is exactly
 * what it looks like and gives no hint that the cause is a column with
 * nowhere to put 78 pixels. It got worse release by release for the obvious
 * reason: every row added to this column — the picker, the ledger, the
 * reading pair — brought the overflow on at a wider screen.
 *
 * The column scrolls. Saying so once, here, is the fix. */
.sidebar > * { flex: none; }

/* The masthead band.
 *
 * Apricot at full strength is the one place the app speaks in its own voice
 * rather than displaying someone else's cover art, so it gets the top of the
 * sidebar and nothing else. It also means the wordmark always sits on the same
 * ground in both themes, which is why its two colours are baked into the file
 * instead of following the theme. */
.brand {
  /* The masthead no longer needs its own ground — the whole column has it now
     — but it keeps the carmine rule under it, which is what separates the name
     from the four destinations.

     That rule and the one under the toolbar beside it used to be two lines:
     different colours, different weights, and — because this block was as
     tall as the wordmark happened to make it — twenty pixels apart. Two
     horizontals that nearly agree read as a mistake, and they were one. They
     are the same line now, the same carmine at the same height, running the
     full width of the window; the height is `--topbar` in both places and the
     mark is sized to fit it rather than the other way round. */
  background: transparent;
  margin: -20px -16px 0;
  padding: var(--safe-top) 16px 0;
  min-height: calc(var(--topbar) + var(--safe-top));
  display: flex; align-items: center;
  border-bottom: 4px solid var(--accent);
  position: relative;
}
.brand .wordmark { margin: 0; line-height: 0; width: 100%; }

/* The name, inline rather than an <img>, so its two halves can be styled.
 *
 * Full width, which is the whole point: the brand block is inset to the same
 * 16px as the four destination buttons below it, so a mark at 100% begins and
 * ends exactly where they do. At a fixed 190px it stopped short of the right
 * edge and read as carelessly placed rather than as deliberately smaller —
 * the column has an edge, and everything in it should either meet that edge
 * or obviously not be trying to. */
/* Full width, which is what squares it up with the four buttons below; the
   height that produces is what `--topbar` is built from. */
.brand .mark { display: block; width: 100%; height: auto; }

/* The one pixel the rule could not reach.
   The sidebar's own right border sits outside this block's box, so the
   carmine band stopped a pixel short of the toolbar's and the "one line
   across the window" had a hairline of panel-rule in the middle of it. Four
   pixels tall and one wide, painted over that border. Found by a test that
   added the two widths together and got one less than the window. */
.brand::after {
  content: ''; position: absolute; right: -1px; bottom: -4px;
  width: 1px; height: 4px; background: var(--accent);
}
.brand .mark .ink-pine { fill: var(--mark-pine); }
.brand .mark .ink-carmine { fill: var(--mark-carmine); }

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

.topbar {
  /* Min-height and wrap, not a fixed height. The bar gained a Showcase button
     and the search box — the only control here whose size is its usefulness —
     was the thing that gave way, shrinking until its placeholder read "Sea". */
  min-height: calc(var(--topbar) + var(--safe-top));
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  /* The safe area is added to the padding, not subtracted from the height.
     Getting that the wrong way round is what put this bar under the dynamic
     island — the same mistake the tab bar made at the other end of the screen. */
  padding: calc(8px + var(--safe-top)) calc(22px + var(--safe-right)) 8px calc(22px + var(--safe-left));
  /* The same rule as the one under the masthead, at the same height. See
     `.brand`. A 1px hairline in a neutral grey beside a 4px carmine band was
     the more obvious half of the mistake. */
  border-bottom: 4px solid var(--accent);
  background: color-mix(in srgb, var(--paper) 86%, transparent);
  backdrop-filter: saturate(1.6) blur(12px);
  position: sticky; top: 0; z-index: 20;
}

/* Both bars are frosted glass, and frosted glass is a thing a phone switches
   off. Low Power Mode and Reduce Transparency both drop `backdrop-filter`,
   and what is left is an 86%-opaque panel with the page legible through it —
   two paragraphs occupying the same pixels, which reads as a rendering fault
   rather than as a setting. Where the blur cannot be had, the bars are
   simply solid. */
@supports not (backdrop-filter: blur(2px)) {
  .topbar { background: var(--paper); }
  .tabbar { background: var(--surface); }
}
@media (prefers-reduced-transparency: reduce) {
  .topbar { background: var(--paper); backdrop-filter: none; }
  .tabbar { background: var(--surface); backdrop-filter: none; }
}

.content { flex: 1; padding: 22px; min-width: 0; }

.settings .build {
  text-align: center; margin: 4px 0 0;
  font-variant-numeric: tabular-nums; letter-spacing: .04em;
}

/* ---------- the library picker ---------- */

/* Built on <details>, so it opens and closes without a line of JavaScript and
 * keeps working if that JavaScript ever fails to arrive. It is empty — and so
 * invisible — until a second library exists. */
.libpicker:empty { display: none; }
.libpicker details { margin: 0; }
.libpicker summary {
  list-style: none; cursor: pointer;
  display: flex; flex-direction: column; gap: 1px;
  border: 1px solid var(--panel-rule);
  border-radius: var(--r-md);
  background: var(--on-panel);
  padding: 7px 14px 8px;
}
.libpicker summary::-webkit-details-marker { display: none; }
.libpicker summary::after {
  content: '▾'; position: absolute; right: 14px; margin-top: 6px;
  color: var(--accent); opacity: .7;
}
.libpicker summary { position: relative; }
.libpicker details[open] summary::after { content: '▴'; }
.lib-eyebrow {
  font-family: var(--sans); font-size: 11px; font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--accent); opacity: .75;
}
.lib-name {
  font-family: var(--logo); font-size: 19px; line-height: 1.05;
  color: var(--accent); padding-right: 16px;
}
/* Pattaya carries no Greek, so a Greek name falls back to a serif and looks
   like a fault. Named as its own state rather than left to the silent
   fallback, and set a little smaller because the reading face runs larger. */
.lib-name.plain {
  font-family: var(--serif, inherit); font-weight: 600;
  font-size: 17px; letter-spacing: 0;
}
.lib-list { display: flex; flex-direction: column; gap: 5px; padding: 7px 0 0; }
.lib-list button {
  text-align: left; width: 100%;
  border: 1px solid transparent; border-radius: var(--r-md);
  background: transparent; color: var(--ink);
  padding: 6px 14px 7px;
}
.lib-list button:hover { background: var(--on-panel); }
.lib-list button[aria-pressed="true"] {
  background: var(--accent); color: var(--accent-ink); border-color: var(--accent);
}
/* The two verbs sit apart from the shelves themselves: one list is where you
   are, the other is what you can do about it. */
.lib-list .lib-act {
  color: var(--accent); font-size: 13px;
  border-top: 1px solid var(--panel-rule); border-radius: 0;
  margin-top: 2px; padding-top: 8px;
}
.lib-list .lib-act + .lib-act { border-top: 0; margin-top: -3px; padding-top: 2px; }

/* Scan, Settings, Stats — the app naming its own rooms, so they are set in the
   name's own face. Every one of these words is Latin, which is the condition
   for using it at all: Pattaya has no Greek glyphs, so a Greek string set in
   it falls back silently and looks like a bug nobody can find. */
.screen-title {
  font-family: var(--logo); font-size: 25px; line-height: 1;
  letter-spacing: 0; color: var(--accent); font-weight: 400;
  padding-bottom: 3px;
}

/* ---------- nav ---------- */

/* The four destinations, each its own button.
 *
 * The other candidate was a name in the logo font over a carmine marker at the
 * column's edge, which was quieter. These won because the sidebar is now one
 * short list and one paragraph: with nothing else in the column competing,
 * four framed buttons read as the app's controls rather than as clutter, and
 * they match Showcase, which is the only other place the app invites you
 * somewhere rather than telling you something. */
.nav { display: flex; flex-direction: column; gap: 7px; }
.nav button {
  display: flex; align-items: center; gap: 11px;
  width: 100%; text-align: left;
  border: 1px solid var(--panel-rule);
  border-radius: var(--r-md);
  background: var(--on-panel); color: var(--accent);
  padding: 8px 14px 9px;
}
/* All four words are Latin, which is the condition for using Pattaya at all:
   it has no Greek glyphs, so a Greek string set in it falls back silently and
   looks like a bug nobody can find. One weight only, so the current
   destination is marked by filling the button rather than by going bold —
   asking a browser to embolden a single-weight script face gives you a
   smeared outline. */
.nav .lbl {
  font-family: var(--logo); font-size: 20px; line-height: 1.25;
  letter-spacing: .005em; padding-bottom: 2px;
}
.nav button:hover { background: var(--surface); }
.nav button[aria-current="true"] {
  background: var(--accent); color: var(--accent-ink); border-color: transparent;
}
.nav button[aria-current="true"]:hover { background: var(--accent-lo); }
.nav button[aria-current="true"] .icon { color: var(--accent-ink); opacity: 1; }
.nav button .badge {
  margin-left: auto; font-size: 11px; font-variant-numeric: tabular-nums;
  background: var(--accent); color: var(--accent-ink);
  border-radius: 999px; padding: 1px 7px; font-weight: 600;
}
/* On the button you are standing on, the badge has to invert or it is carmine
   on carmine — which is how a queue of nine books becomes invisible exactly
   while you are looking at the screen that queues them. */
.nav button[aria-current="true"] .badge {
  background: var(--accent-ink); color: var(--accent);
}

/* Add: the one button in the column that does something.
   Set a little apart from the three destinations above it, and in pine,
   because carmine already means "you are here" — an action dressed as a
   location would read as one. Being on the Add screen is shown by a ring
   rather than by a change of colour, so the button keeps looking like what
   it is while you are using it. */
.nav .nav-add {
  margin-top: 10px;
  background: var(--good); color: var(--accent-ink); border-color: transparent;
}
.nav .nav-add .icon { color: var(--accent-ink); opacity: 1; font-weight: 700; }
.nav .nav-add:hover { background: color-mix(in srgb, var(--good) 86%, black); }
.nav .nav-add[aria-current="true"] {
  background: var(--good); color: var(--accent-ink);
  box-shadow: inset 0 0 0 2px var(--accent-ink), 0 0 0 2px var(--good);
}
.nav .nav-add[aria-current="true"]:hover { background: color-mix(in srgb, var(--good) 86%, black); }
.nav .nav-add .badge { background: var(--accent-ink); color: var(--good); }
/* The four marks beside the destinations, and why one of them needed saying.
 *
 * ▦ ◔ ◉ are geometric shapes and no font has ever had a second opinion about
 * them. ⚙ is in the emoji set as well, so a phone draws it from the emoji
 * font — a shaded, bevelled, full-colour gear — and one sticker in a row of
 * four flat marks is the kind of thing you notice without being able to say
 * what changed. U+FE0E after the character asks the font for the text form
 * and `font-variant-emoji` asks the browser for it; between them they cover
 * every engine that has an opinion, and both are inert where there is only
 * one form to begin with. */
.nav .icon, .tabbar .icon { font-variant-emoji: text; }
.nav .icon { width: 17px; text-align: center; opacity: .9; }

/* ---------- the library, in a sentence ---------- */

/* What the sidebar says now.
 *
 * It held five scrolling lists of counts — Language, Subject, Room, Publisher,
 * Decade — which were a worse copy of the Stats page and, stacked in a 268px
 * column, five scroll areas fighting each other. Every one of those filters is
 * a bar on the Stats page, so nothing could be done there that cannot be done
 * here. What is left is the thing a chart page can never be: something true
 * about the library, in front of you on every screen, that you read rather
 * than operate.
 *
 * The figures are set in the name's own face. Digits are the one string
 * Pattaya can always be trusted with — it has all ten and no Greek at all. */
/* The same separator the rest of this column uses: one rule, full width of
   what it divides. */
.overview {
  display: flex; flex-direction: column; gap: 2px;
  border-top: 1px solid var(--panel-rule); padding-top: 16px;
  margin: 0 -16px; padding-left: 16px; padding-right: 16px;
}
.overview .ov-note {
  margin: 0 0 6px; font-size: 11.5px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted);
}
.overview .fig { display: flex; align-items: baseline; gap: 9px; }
.overview .fig b {
  font-family: var(--logo); font-weight: 400;
  font-size: 30px; line-height: 1.18; color: var(--accent);
  letter-spacing: 0;
}
/* Each figure after the first steps down, so the column reads as one statement
   with a subject rather than as three competing headlines. */
.overview .fig:nth-of-type(2) b { font-size: 21px; color: var(--slate); }
.overview .fig:nth-of-type(3) b { font-size: 21px; color: var(--slate); }
.overview .fig span {
  font-size: 13.5px; color: var(--ink-soft); letter-spacing: .01em;
}
.overview .ov-line {
  margin: 0; padding-top: 9px; font-size: 14px; line-height: 1.55;
  color: var(--ink-soft); text-wrap: pretty;
}
.overview .ov-line + .ov-line { padding-top: 2px; }

/* The two reading marks, side by side and the width of the column between
   them. They are the most-asked questions of a library — what have I read,
   what is waiting — and they were four taps away through a chart. Pine for
   read, carmine for the pile, matching the marks on a book's own page. */
/* The same width as the rows beneath them.
   `.ov-act` bleeds 8px into the sidebar's padding on each side so that a full
   row reads as a band across the column; these two did not, so the pair sat
   16px narrower than everything under it and the column had two left edges. */
.overview .ov-marks {
  /* As many equal columns as there are marks, which matters because there are
     often not two: a library with nothing marked "want to read" showed a
     single half-width box beside a gap, and every row under it ran the full
     width of the column. */
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 6px;
  margin: 12px -8px 0; width: calc(100% + 16px);
}
.overview .ov-marks:only-child { margin-block-start: 0; }
.overview .ov-mark {
  display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
  padding: 8px 11px 9px; border-radius: var(--r-md);
  border: 1px solid var(--panel-rule); background: var(--on-panel);
  text-align: left; line-height: 1.15;
}
.overview .ov-mark b {
  font-family: var(--logo); font-weight: 400; font-size: 20px; letter-spacing: 0;
}
.overview .ov-mark span { font-size: 13px; color: var(--ink-soft); }
.overview .ov-mark.read b { color: var(--slate); }
.overview .ov-mark.want b { color: var(--accent); }
.overview .ov-mark.read { border-color: color-mix(in srgb, var(--slate) 34%, var(--panel-rule)); }
.overview .ov-mark.want { border-color: color-mix(in srgb, var(--accent) 34%, var(--panel-rule)); }
.overview .ov-mark:hover { background: var(--surface); }
.overview .ov-mark[aria-pressed="true"] { border-color: transparent; color: var(--accent-ink); }
.overview .ov-mark.read[aria-pressed="true"] { background: var(--slate); }
.overview .ov-mark.want[aria-pressed="true"] { background: var(--accent); }
.overview .ov-mark[aria-pressed="true"] b,
.overview .ov-mark[aria-pressed="true"] span { color: var(--accent-ink); }

/* The two lines that are about something to do rather than something to know:
   the books still waiting to be named, and the lists you keep. Nothing else
   here is a control, which is what lets these read as one. */
.overview .ov-act {
  display: block; width: 100%; text-align: left;
  margin: 9px -8px 0; width: calc(100% + 16px);
  border: 1px solid var(--panel-rule); background: var(--on-panel);
  border-radius: var(--r-md); padding: 7px 10px;
  font-size: 14px; color: var(--ink-soft);
}
.overview .ov-act + .ov-act { margin-top: 5px; }
.overview .ov-act b {
  font-family: var(--logo); font-weight: 400; font-size: 18px;
  color: var(--accent); margin-right: 4px; letter-spacing: 0;
}
.overview .ov-act:hover { background: var(--surface);
  border-color: color-mix(in srgb, var(--accent) 45%, var(--panel-rule)); }
.overview .ov-act[aria-pressed="true"] {
  background: var(--accent); color: var(--accent-ink); border-color: transparent;
}
.overview .ov-act[aria-pressed="true"] b { color: var(--accent-ink); }

/* The curator's lists, as a level of their own.
   Set as an index rather than as buttons: the name leads, left, in the face
   the book titles use; the count trails, right, in the face of every other
   figure in this column. That is the shape of a contents page, which is what
   this is, and it is visibly not the shape of the two banded rows above,
   which are the library telling you something.

   At the size of a title, not of a caption. They were set at 14.5px in v55,
   the size of the small print around them, and read as small print: a list
   is a place you go, and it should be as easy to hit as the destinations at
   the top of the column. */
.ov-lists { margin-top: 20px; }
.ov-lists-head {
  display: flex; align-items: baseline; justify-content: space-between;
  border-top: 1px solid var(--panel-rule); padding-top: 16px; margin-bottom: 6px;
}
.ov-lists-head h3 {
  margin: 0; font-family: inherit; font-size: 13px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--ink-soft);
}
.ov-manage {
  border: none; background: none; padding: 6px 0; font-size: 14.5px;
  color: var(--ink-soft); text-decoration: underline;
  text-decoration-color: var(--panel-rule); text-underline-offset: 3px;
}
.ov-manage:hover { color: var(--accent); text-decoration-color: var(--accent); }
.ov-list {
  display: flex; align-items: baseline; gap: 12px; width: calc(100% + 16px);
  margin: 0 -8px; padding: 9px 8px 10px; border: none; background: none;
  border-radius: var(--r-md); text-align: left; color: var(--ink);
  min-height: 44px;
}
.ov-list + .ov-list { margin-top: 2px; }
.ov-list .nm {
  flex: 1; min-width: 0; overflow-wrap: anywhere;
  font-family: var(--serif); font-size: 18px; font-weight: 550; line-height: 1.25;
}
.ov-list .ct {
  font-family: var(--logo); font-weight: 400; font-size: 19px; line-height: 1;
  color: var(--accent); font-variant-numeric: tabular-nums;
}
.ov-list:hover { background: var(--on-panel); }
.ov-list[aria-pressed="true"] { background: var(--slate); color: var(--accent-ink); }
.ov-list[aria-pressed="true"] .ct { color: var(--accent-ink); }

/* Renaming the list you are reading, from the row of chips above it.
   The link alone until it is used; the field and its two buttons only while
   renaming. v55 had them all showing at once, because `display: inline-flex`
   on the class outranked the `hidden` attribute that was meant to put them
   away — see the rule on [hidden] at the top of the sheet. */
.list-tools { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.list-tools .linkish { font-size: 15px; }
.list-rename { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.list-rename input { width: min(260px, 60vw); padding: 7px 10px; font-size: 15px; }

/* ---------- view switcher ---------- */

/* The view picker is the control the eye should find first — it is how you
   change what the whole screen is — so it is the one piece of chrome that runs
   on apricot rather than on paper. The track carries the identity, the chosen
   view carries carmine, and each view is named with the shape it produces. */
.segmented {
  display: inline-flex;
  background: color-mix(in srgb, var(--apricot) 62%, var(--surface));
  border-radius: var(--r-md); padding: 4px; gap: 2px;
  border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--rule));
}
.segmented button {
  border: none; background: transparent; border-radius: 6px;
  /* `line-height: 1` is what keeps the carmine pill inside the apricot strip.
     Without it the buttons inherit 1.6 and stand two pixels taller than the
     container that is supposed to hold them, so the selected one bulged over
     the top and bottom edges and read as a control that had grown wrong. */
  padding: 9px 15px 7px; font-size: 15px; font-weight: 600; line-height: 1;
  color: color-mix(in srgb, var(--accent) 72%, var(--ink));
  display: inline-flex; align-items: center; gap: 6px;
}
.segmented button .icon {
  font-size: 14px; line-height: 1; opacity: .85;
  /* A transform does nothing to a non-replaced inline element, which is why
     the first attempt at this changed nothing at all. */
  display: inline-block; transform: translateY(-1.5px);
}
.segmented button:hover { background: color-mix(in srgb, var(--surface) 62%, transparent); }
.segmented button[aria-pressed="true"] {
  background: var(--accent); color: var(--accent-ink); box-shadow: none; font-weight: 700;
}
.segmented button[aria-pressed="true"] .icon { opacity: 1; }
.segmented button[aria-pressed="true"]:hover { background: var(--accent-lo); }

/* ---------- search ---------- */

/* Grows into whatever room is left, but never below the width at which the
   placeholder stops being a sentence. */
.searchbox { position: relative; flex: 1 1 260px; min-width: 190px; max-width: 460px; }
.searchbox input {
  padding-left: 34px; border-radius: 999px; background: var(--surface);
}
.searchbox .glyph {
  position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
  color: var(--muted); pointer-events: none; font-size: 14px;
}
.searchbox .clear {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  border: none; background: transparent; padding: 4px 8px; color: var(--muted);
  border-radius: 999px;
}

/* ---------- the cover wall ---------- */

.wall {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(126px, 1fr));
  gap: 26px 20px;
  align-items: end;
}
.wall.tight { grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: 18px 14px; }
.wall.large { grid-template-columns: repeat(auto-fill, minmax(196px, 1fr)); gap: 34px 26px; }
/* Smallest: the whole shelf at a glance, covers only. At this width a caption
   is two words of a title and adds nothing a cover does not already say, so
   it is taken off the screen — but kept for anything reading the page aloud,
   where the tile is otherwise a button with no name. */
.wall.tiny  { grid-template-columns: repeat(auto-fill, minmax(62px, 1fr)); gap: 12px 9px; }
.wall.tiny .tile .meta {
  position: absolute; width: 1px; height: 1px; min-height: 0; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
.wall.tiny .binding .b-author { display: none; }

.tile {
  /* Keeps the wall smooth at 3,000 tiles: the browser skips layout and paint
     for anything off screen, but still reserves the right space for it. */
  content-visibility: auto;
  contain-intrinsic-size: auto 260px;
  cursor: pointer; border: none; background: none; padding: 0;
  display: flex; flex-direction: column; gap: 8px; text-align: left;
  border-radius: 0;
}
.tile .art {
  position: relative; width: 100%; aspect-ratio: 2 / 3;
  border-radius: 2px 4px 4px 2px;
  overflow: hidden;
  box-shadow: var(--shadow-book);
  transition: transform .16s cubic-bezier(.2,.7,.3,1), box-shadow .16s ease;
  /* the faint impression of a spine down the left edge */
  background: var(--surface-2);
}
.tile .art::after {
  content: ''; position: absolute; inset: 0 auto 0 0; width: 7px;
  background: linear-gradient(90deg, rgba(0,0,0,.20), rgba(0,0,0,.06) 55%, rgba(255,255,255,.07));
  pointer-events: none;
}
.tile:hover .art { transform: translateY(-5px) rotate(-.4deg); box-shadow: var(--shadow-2); }
.tile:focus-visible .art { outline: 2px solid var(--accent); outline-offset: 3px; }
.tile .art img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Generated binding for books with no cover art. Colour comes from a hash of
   the publisher, so a shelf of Ίκαρος paperbacks looks like a set. */
.binding {
  width: 100%; height: 100%;
  display: flex; flex-direction: column; justify-content: space-between;
  padding: 13px 11px; gap: 8px;
  font-family: var(--serif); color: rgba(255,255,255,.94);
  background-image:
    linear-gradient(160deg, rgba(255,255,255,.09), rgba(0,0,0,.14)),
    var(--cloth, linear-gradient(#6b5344, #4d3b30));
  position: relative;
}
.binding::before {
  /* a hairline rule, the way a cloth binding is stamped */
  content: ''; position: absolute; inset: 7px; border: 1px solid rgba(255,255,255,.22);
  border-radius: 1px; pointer-events: none;
}
/* The stamped title follows the wall's type too, or a cloth binding on the
   large wall carries lettering sized for the small one. Outside a wall — the
   book sheet's own cover — the fallback applies. */
.binding .b-title {
  font-size: var(--tile-type, 13px); line-height: 1.22; font-weight: 700; letter-spacing: .002em;
  display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden;
  z-index: 1;
}
.binding .b-author {
  font-size: calc(var(--tile-type, 13px) * 0.77);
  opacity: .82; text-transform: uppercase; letter-spacing: .07em;
  font-family: var(--sans); z-index: 1;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* Every tile is the same height, whatever its book is called.
 *
 * The title is clamped to two lines and the author to one, but a book with a
 * one-line title used to produce a shorter tile — so the wall came out ragged,
 * and in Showcase the ✕ and "next" moved between one book and the next, which
 * meant chasing the button around the screen. The space for two lines and an
 * author is reserved whether or not the book fills it. */
/* The type scales with the wall.
   Every density used to set the title at 13px, so Large drew 196px covers with
   the same caption as Small drew under 88px ones — which made the big wall
   look like the small one photographed closer. One variable per density, and
   because everything below is in em the whole block follows it. */
.tile .meta {
  padding: 0 1px;
  min-height: calc(2 * 1.3em + 1.55 * 0.885em + 2px);   /* two title lines + one author line */
  font-size: var(--tile-type, 13px);
}
.wall.tight { --tile-type: 12px; }
.wall.tiny  { --tile-type: 9.5px; }
.wall        { --tile-type: 15px; }
.wall.large  { --tile-type: 17.5px; }
.tile .t {
  font-family: var(--serif); font-size: 1em; line-height: 1.3; font-weight: 550;
  min-height: 2.6em;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.tile .a {
  font-size: 0.9em; color: var(--muted); margin-top: 3px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tile .flag {
  position: absolute; top: 7px; right: 7px; z-index: 2;
  background: var(--warn); color: #fff; font-size: 11px; font-weight: 700;
  padding: 2px 6px; border-radius: 999px; letter-spacing: .04em;
  box-shadow: 0 1px 3px rgba(0,0,0,.3);
}
.tile .art .spinner { position: absolute; inset: 0; display: grid; place-items: center; }

/* ---------- table ---------- */

/* The table scrolls inside its own pane, and that is what makes the headings
   work at all.
 *
 * `overflow-x: auto` here — which the table has always needed, because nine
 * columns do not fit a phone — makes this element the scroll container, and a
 * `position: sticky` heading inside it is therefore positioned against *this
 * box*, not the window. It was offset by `--topbar`, a number that means
 * something to the window and nothing here, so the heading row was pushed
 * eighty pixels down from the top of the table and rendered on top of the
 * first two books. And it could never stick to anything, because the box it
 * was sticking inside did not scroll.
 *
 * So the box scrolls. A bounded height turns it into a real pane: the
 * headings stay at the top of it through three thousand rows, which is the
 * only way anybody discovers they can be clicked. */
.table-wrap {
  overflow: auto;
  max-height: calc(100dvh - var(--topbar) - 4px - 44px);
  border: 1px solid var(--rule); border-radius: var(--r-lg); background: var(--surface);
  overscroll-behavior: contain;
}
table.books { width: 100%; border-collapse: collapse; font-size: 15px; }

/* The headings: the app's own face, on a band of its own.
 *
 * Small pale uppercase said "caption". These are controls — the only way to
 * order three thousand books — so they are set in the face the rest of the
 * app uses for the things you press, in carmine, on a warm band that makes
 * the row read as a bar rather than as a first line of data. Sentence case,
 * not caps: a script face in caps with letter-spacing is a ransom note.
 *
 * Every label here is Latin (Title, Author, Publisher, Year, Pages, Lang,
 * Shelf, ISBN) and has to stay that way — Pattaya has no Greek glyphs and
 * falls back silently, which is the app's oldest trap. The browser suite
 * checks the face actually applied. */
table.books th {
  position: sticky; top: 0; z-index: 5;
  background: color-mix(in srgb, var(--apricot) 42%, var(--surface));
  text-align: left; white-space: nowrap; cursor: pointer; user-select: none;
  font-family: var(--logo); font-weight: 400; font-size: 18px; line-height: 1;
  letter-spacing: 0; text-transform: none;
  color: color-mix(in srgb, var(--accent) 78%, var(--ink));
  padding: 13px 13px 15px;
  border-bottom: 2px solid color-mix(in srgb, var(--accent) 30%, var(--rule));
}
table.books th .lbl { vertical-align: middle; }
table.books th:hover { color: var(--accent); background: color-mix(in srgb, var(--apricot) 62%, var(--surface)); }
table.books th.on { color: var(--accent); }
table.books th.on::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -2px;
  height: 2px; background: var(--accent);
}
/* The mark that says a column can be ordered at all. Faint until it is the
   one in use, and never absent: a heading with no mark reads as a caption. */
table.books th .dir {
  font-family: var(--sans); font-size: 12px; margin-left: 6px;
  opacity: .45; vertical-align: middle; display: inline-block;
}
table.books th.on .dir { opacity: 1; }
table.books th:hover .dir { opacity: .8; }
table.books td { padding: 8px 13px; border-bottom: 1px solid var(--rule-soft); vertical-align: middle; }
/* 56, not 41: the guess was fifteen pixels short of a real row, which is how
   a table of three thousand of them ends up several screens taller than the
   scrollbar thinks it is. */
table.books tbody tr { content-visibility: auto; contain-intrinsic-size: auto 56px; cursor: pointer; }
table.books tbody tr:hover { background: var(--surface-2); }
/* Scoped to the cells, which they always meant.
 *
 * A heading carries its column's class too, and `table.books .c-title` beats
 * `table.books th` on specificity — so the Title heading was quietly set in
 * the reading serif while the seven beside it were in the app's own face, the
 * numeric headings were right-aligned by accident of the same rule, and the
 * rest were painted in the muted grey meant for their data. The app's oldest
 * trap, found by a test that asked which face had actually been used rather
 * than which one had been asked for. */
table.books td.c-title { font-family: var(--serif); font-weight: 550; min-width: 220px; }
table.books td.c-cover { width: 34px; padding-right: 0; }
table.books .c-cover .mini {
  width: 26px; height: 39px; border-radius: 1px 2px 2px 1px; object-fit: cover;
  box-shadow: 0 1px 2px rgba(0,0,0,.22); display: block;
}
table.books td.c-num { text-align: right; font-variant-numeric: tabular-nums; color: var(--muted); white-space: nowrap; }
table.books td.c-sub { color: var(--muted); }
/* The numeric headings still line up over their own column — deliberately
   this time, rather than by inheriting a rule written for the cells. */
table.books th.c-num { text-align: right; }
/* An ISBN is one thing, and it was breaking at its own hyphens into three
   lines — which made the row it was in half as tall again as its neighbours. */
table.books td.c-sub.mono { white-space: nowrap; }

/* ---------- shelves, as bookcases ---------- */

/* Each group is drawn as a bookcase: the name and the count above, then one
   row of covers standing on a ledge — the carmine rule the masthead uses,
   thickened to a plank. The row scrolls sideways, as you would walk along a
   real one, and stops at thirty-six with a door to the rest on the wall. */
.cases { display: flex; flex-direction: column; gap: 34px; }
.case { --cover: 104px; }
.case-head {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 14px; margin-bottom: 12px;
}
.case-name {
  margin: 0; padding: 0; border: 0; background: none; text-align: left;
  font-family: var(--serif); font-size: 24px; font-weight: 700; line-height: 1.15;
  letter-spacing: -.01em; color: var(--ink);
}
button.case-name { cursor: pointer; text-decoration: underline; text-decoration-color: transparent;
  text-underline-offset: 4px; transition: text-decoration-color .15s; }
button.case-name:hover { color: var(--accent); text-decoration-color: currentColor; }
.case-n { font-size: 14px; color: var(--muted); font-variant-numeric: tabular-nums; }
.case-who { flex-basis: 100%; font-size: 14px; color: var(--ink-soft); }
.case-row {
  display: flex; align-items: flex-end; gap: 14px;
  overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: thin;
  padding: 4px 2px 0;
  border-bottom: 7px solid color-mix(in srgb, var(--accent) 78%, var(--ink));
  box-shadow: 0 6px 0 -3px color-mix(in srgb, var(--ink) 18%, transparent);
  scroll-snap-type: x proximity;
}
.case-row .tile { flex: 0 0 var(--cover); width: var(--cover); content-visibility: visible; scroll-snap-align: start; }
/* On a ledge the captions are the spines' business, not the row's: one line
   of title, no author, so the covers stand in a line. */
.case-row .tile .meta { min-height: 0; font-size: 12.5px; padding-bottom: 8px; }
.case-row .tile .t { -webkit-line-clamp: 1; min-height: 0; }
.case-row .tile .a { display: none; }
.case-more {
  flex: 0 0 var(--cover); align-self: stretch; margin-bottom: 8px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  border: 1.5px dashed var(--rule); border-radius: 4px; background: none; color: var(--ink-soft);
  font-size: 13px; text-align: center; padding: 8px;
}
.case-more b { font-family: var(--logo); font-weight: 400; font-size: 26px; color: var(--accent); }
button.case-more:hover { border-color: var(--accent); color: var(--accent); }
/* The books not placed yet: the same furniture, drawn as unfinished. */
.case-blank .case-name { color: var(--ink-soft); font-style: italic; font-weight: 500; }
.case-blank .case-row { border-bottom-style: dashed; border-bottom-width: 3px;
  border-bottom-color: var(--rule); box-shadow: none; }

@media (max-width: 860px) {
  .case { --cover: 84px; }
  .case-name { font-size: 21px; }
}

/* The Add screen's "shelving into": a line under the buttons that turns pine
   while it is on, because everything scanned is going somewhere it says. */
.filing {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 14px; padding: 10px 14px; border-radius: var(--r-md);
  border: 1px solid var(--rule); background: var(--surface);
}
.filing label { margin: 0; font-size: 14px; font-weight: 600; color: var(--ink-soft); }
.filing input { flex: 1; min-width: 160px; }
.filing.on { border-color: color-mix(in srgb, var(--good) 45%, var(--rule));
  background: color-mix(in srgb, var(--good) 9%, var(--surface)); }
.filing.on label { color: var(--good); }
.scan-into {
  background: var(--good); color: #fff; border-radius: 999px; padding: 3px 10px;
  font-size: 13px; font-weight: 600; white-space: nowrap; max-width: 45%;
  overflow: hidden; text-overflow: ellipsis;
}

.strip-foot { margin: 12px 0 0; font-size: 14.5px; }

/* ---------- stats ---------- */

/* The card grid the statistics used to be. `.stats` survives as the hook
   every test and handler already reaches for; the grid itself does not,
   because the page is strips now and a grid here would fight them. */
.stats:not(.stats-page) { display: grid; grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); gap: 20px; }
.card {
  background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--r-lg); padding: 19px 20px;
}
.card h3 {
  font-family: var(--sans); font-size: 12.5px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-soft); margin-bottom: 15px;
}
.bignum { font-family: var(--serif); font-size: 40px; line-height: 1; letter-spacing: -.02em; }
.bignum + .sub { color: var(--ink-soft); font-size: 15px; margin-top: 6px; }
/* The whole library, in the name's own face. Pattaya sits small on its body
   and has a deep descender, so it needs more size and room than the serif did.
   `--bignum-ink` is the one thing to change to try the other colour: carmine
   here, pine by swapping the token below. */
.bignum.logo {
  font-family: var(--logo); font-size: 58px; line-height: 1.05;
  letter-spacing: 0; color: var(--bignum-ink, var(--accent));
  margin: 2px 0 -4px;
}

.bars { display: flex; flex-direction: column; gap: 9px; }
.bar-row { display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 10px; align-items: center; font-size: 14.5px; }
.bar-row .label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bar-row .n { font-variant-numeric: tabular-nums; color: var(--muted); font-size: 13.5px; }
.bar-track { grid-column: 1 / -1; height: 5px; background: var(--surface-2); border-radius: 999px; overflow: hidden; margin-top: -4px; }
.bar-fill { height: 100%; background: var(--accent); border-radius: 999px; }

.spark { display: flex; align-items: flex-end; gap: 3px; height: 92px; }
.spark .col { flex: 1; background: var(--slate); border-radius: 2px 2px 0 0; min-height: 2px; position: relative; }
.spark .col:hover { background: var(--accent); }
.spark-labels { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); margin-top: 7px; }

/* A bar that says which decade it is.
   The chart used to letter only its two ends, which left every column between
   them to be counted along by eye — the one job a chart exists to save you.
   Each column now owns its label, so the slot is the flex child and the bar
   grows inside it. */
.spark.ticked { align-items: stretch; height: 116px; }
/* A grid, not a flex column, and that is the whole trick: `1fr auto` gives the
   bar a row of its own with a definite height, so a bar at 100% fills its row
   and the label still has somewhere to be. As a flex column the tallest bar
   asked for the entire slot and squeezed its own label out of existence —
   which is how the chart came to letter every decade except the biggest one. */
.spark.ticked .col-slot {
  flex: 1; min-width: 0; display: grid; grid-template-rows: 1fr auto; gap: 5px;
}
.spark.ticked .col { align-self: end; }
.spark.ticked .tick {
  font-size: 11px; color: var(--muted); text-align: center;
  font-variant-numeric: tabular-nums; letter-spacing: -.03em;
  white-space: nowrap; overflow: visible;
}
/* Where the decades outrun the room, every other label steps back rather than
   colliding with its neighbour — the ones left still fix the scale. */
.spark.ticked .tick.thin { visibility: hidden; }
@media (min-width: 1240px) { .spark.ticked .tick.thin { visibility: visible; } }

/* A statistic you can walk through.
 *
 * Every row and column on the Stats page is a button now, so it has to keep
 * looking like a line of a chart and start behaving like a link: no chrome at
 * rest, an obvious target under a finger. The 44px minimum is deliberate —
 * these are read standing in front of a bookcase, one-handed. */
.bar-item {
  display: block; width: 100%; text-align: left; background: none;
  border: 0; padding: 3px 4px; margin: -3px -4px; border-radius: 7px;
  color: inherit; font: inherit; cursor: default;
}
button.bar-item { cursor: pointer; }
button.bar-item:hover { background: var(--surface-2); }
button.bar-item:hover .bar-fill { background: var(--accent-strong, var(--accent)); }
button.bar-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.spark .col { border: 0; padding: 0; }
button.col { cursor: pointer; }

/* A number in the summary card that opens the shelf behind it. */
.linkish {
  background: none; border: 0; padding: 0; font: inherit; color: inherit;
  cursor: pointer; text-decoration: underline; text-decoration-style: dotted;
  text-underline-offset: 3px; text-decoration-color: var(--muted);
}
.linkish:hover { color: var(--accent); text-decoration-color: var(--accent); }
/* The author under a title is the strongest door on a book's page, so it keeps
   the size and colour of a byline and gains only the dotted rule the other
   doors wear. A page of underlined links would read as a form. */
.sheet .byline .linkish { font-size: inherit; color: inherit; }
.sheet .byline .linkish:hover { color: var(--accent); }

/* What is being looked at, and the way back out.
 *
 * The sidebar has rows for languages and rooms but none for authors or
 * subjects, so a filter set by tapping a statistic had no visible off switch.
 * These chips are that switch, and they double as the answer to "why am I
 * looking at 34 books?" */
/* What is narrowing the view, and the way out of it.
 *
 * Sized to be read from across a desk, like the view picker and Showcase
 * beside them: these say what you are looking at, and at twelve and a half
 * pixels of grey they were the quietest thing on a screen full of book
 * covers. */
.filter-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 9px; margin: 0 0 16px; }
.filter-chip {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 8px 14px; border-radius: 999px; font-size: 14.5px;
  background: var(--surface-2); border: 1px solid var(--rule); color: var(--ink);
  cursor: pointer; max-width: 100%;
}
.filter-chip .k { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .06em; }
/* The ✕ was eleven pixels of grey and it was the only way out of a filter.
   A round target, ink rather than muted, and it lights up under the finger —
   it is a control, and it should have looked like one.

   The mark inside it is an SVG and not the character. A typeface puts `✕`
   wherever it likes on its em box, so flex-centring the glyph centres a box
   with the mark sitting a pixel high and left inside it; two strokes in a
   square viewBox have no such opinion. */
.filter-chip .x {
  color: var(--ink-soft); line-height: 0;
  display: inline-flex; align-items: center; justify-content: center;
  width: 19px; height: 19px; border-radius: 50%;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
}
.filter-chip .x svg { display: block; }
.filter-chip:hover { border-color: var(--accent); }
.filter-chip:hover .x { background: var(--accent); color: var(--accent-ink); }

/* Home. Pine rather than carmine, and beside the carmine chips on purpose:
   every chip is something switched on, this is the switch that turns them all
   off, and it should not look like another one of them. Showcase is the same
   green for the same reason — it is the other control here that goes
   somewhere rather than filtering. */
.go-home {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 16px 9px; border-radius: 999px;
  font-family: var(--logo); font-size: 18px; line-height: 1;
  background: var(--surface); color: var(--slate);
  border: 1px solid color-mix(in srgb, var(--slate) 38%, var(--rule));
  cursor: pointer; white-space: nowrap;
}
.go-home .arrow { font-family: var(--sans); font-size: 15px; line-height: 1; }
.go-home b { font-weight: 400; font-variant-numeric: tabular-nums; }
.go-home:hover { background: var(--slate); color: var(--accent-ink); border-color: transparent; }

/* The card's action bar has to hold a carousel on a phone as well as the
   ordinary Edit/Remove row, so it wraps rather than pushing anything off the
   edge — the lesson from the tab bar. */
.sheet-actions { flex-wrap: wrap; }
.sheet-actions .nav-pos { font-variant-numeric: tabular-nums; margin-right: 4px; }
/* A ‹ or › is a narrow glyph in a wide button, so centring it needs saying —
   left to the text padding it sits against one edge. */
.sheet-actions [data-act="prev"], .sheet-actions [data-act="next"] {
  min-width: 46px; padding: 7px 10px; display: inline-flex;
  align-items: center; justify-content: center; font-size: 17px; line-height: 1;
}

/* ---------- what the queue is holding ---------- */

/* Three states, named rather than colour-coded, because the difference
   between "asking now" and "waiting to ask again" is the difference between
   a queue that is working and one that is stuck — and that is exactly what a
   bare number could not tell you. */
.qlist { max-height: 300px; overflow-y: auto; border: 1px solid var(--rule);
  border-radius: var(--r-md); background: var(--surface-2); padding: 8px; }
.qrow { display: grid; grid-template-columns: 82px minmax(0, 1fr); gap: 4px 10px;
  padding: 6px 6px; border-radius: var(--r-sm); font-size: 14px; align-items: baseline; }
.qrow + .qrow { border-top: 1px solid var(--rule-soft); }
.qstate { font-size: 11.5px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--muted); }
.qstate.waiting { color: var(--warn); }
.qname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qwhy { grid-column: 2; font-size: 12.5px; color: var(--muted); }

/* The toolbar's own controls, sized as a set.
   A <select> is as wide as its widest option, so Sort — which has "Recently
   added" in it — came out two and a half times the width of Size while
   showing the word "Title". The width of a menu should say how important it
   is, not how long its longest entry happens to be, so both are given the
   same one and the long option is clipped in the closed state only. */
#library-tools select { width: 112px; flex: 0 0 auto; }
/* The grouping menu appears alone, on Shelves, and its entries are two words
   each — it has no neighbour to match, so it is given room to say them. */
#library-tools #group-picker { width: 140px; }
#library-tools > button, #library-tools > select,
#library-tools > .segmented { height: 38px; }
#library-tools > button { display: inline-flex; align-items: center; }
/* The pill fills the strip exactly, rather than being centred in it.
   The buttons used to be 31px tall in a 28px strip, so they overflowed it by
   a pixel and a half each way — and how a browser places an item that does
   not fit is not something every browser agrees on: Chrome on a Mac pushed
   the carmine pill against the top edge. Now each button is the strip's
   height, the strip's 4px padding is the same margin all round, and the
   label sits a pixel low inside it (2px on top, none below) — the optical
   middle the old 9/7 padding was there to find. */
#library-tools > .segmented { align-items: stretch; }
#library-tools > .segmented button { padding-block: 2px 0; height: auto; }

/* ---------- the buttons that invite rather than instruct ---------- */

/* Three of these in the app: Showcase, and the two on the scanning screen.
   They are the places where the app asks you to do something rather than
   offering a setting, so they wear the name's own face. Latin only — see
   --logo — and one weight, so emphasis comes from the fill and never from
   asking a browser to embolden a script face.

   Pattaya sits small on its body and carries a deep descender, so these are
   sized and padded from their own baseline rather than inheriting a button's. */
.logo-btn {
  font-family: var(--logo); font-weight: 400; letter-spacing: .01em;
  font-size: 20px; line-height: 1;
  padding: 11px 20px 13px;
  display: inline-flex; align-items: center; justify-content: center;
}
/* Carmine fills the one that starts the camera: on this screen it is the whole
   point of the screen. */
.logo-btn.go {
  background: var(--accent); color: var(--accent-ink); border-color: transparent;
}
.logo-btn.go:hover { background: var(--accent-lo); }
/* Pine, outlined, for the way in that does not need the camera. */
.logo-btn.alt {
  background: var(--surface); color: var(--slate);
  border-color: color-mix(in srgb, var(--slate) 38%, var(--rule));
}
.logo-btn.alt:hover { background: var(--slate); color: var(--accent-ink); border-color: transparent; }
/* Once the camera is running the same button stops it, and a stop is not an
   invitation — it empties out and keeps only its outline. */
.logo-btn.go.stop {
  background: var(--surface); color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 38%, var(--rule));
}
.logo-btn.go.stop:hover { background: color-mix(in srgb, var(--accent) 10%, var(--surface)); }

/* Showcase, in pine.
   It was carmine, and carmine now belongs to the four destinations in the
   sidebar and to the chosen view in the picker directly beside it — so the one
   invitation in the toolbar had become one more carmine-outlined box in a row
   of them. Pine is the identity's other half and is used nowhere else as a
   control, which is exactly what makes it read as a different kind of thing.
   Sized by height rather than padding, to sit level with its neighbours. */
#showcase-btn {
  font-family: var(--logo); font-size: 23px; line-height: 1;
  /* Optically centred, not metrically. Pattaya is a slanted script: the ink
     of "Showcase" overhangs its advance width on the right, so a box centred
     on the text's *advance* puts the word three pixels right of the middle of
     the button — measured at 68 device pixels of gap on the left against 60
     on the right, which is small and, once seen, impossible to unsee. The
     padding is asymmetric by exactly that much — two pixels, arrived at by
     measuring, not by eye: three overshot it the other way. It is tied to
     this word in this face, so the browser suite measures the painted ink and
     fails if either changes. */
  padding: 5px 21px 1px 19px; color: var(--slate);
  justify-content: center;
  border-color: color-mix(in srgb, var(--slate) 38%, var(--rule));
}
#showcase-btn:hover { background: var(--slate); color: var(--accent-ink); border-color: transparent; }

/* ---------- copies, and other editions ---------- */

/* The count the scanner keeps, made correctable. It sits in the details list
   like any other field, so it reads as a fact about the book rather than a
   control bolted on — but every part of it is adjustable, because the number
   is a guess the camera made and guesses need correcting. */
.copies { display: inline-flex; align-items: center; gap: 8px; }
.copies b { font-variant-numeric: tabular-nums; min-width: 12px; text-align: center; font-weight: 600; }
.copies button {
  width: 24px; height: 24px; padding: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 14px; line-height: 1; color: var(--ink-soft);
}
.tile .copies-badge {
  position: absolute; left: 7px; bottom: 7px; z-index: 2;
  background: var(--slate); color: #fff; font-size: 11px; font-weight: 700;
  padding: 1px 6px; border-radius: 999px; letter-spacing: .02em;
  box-shadow: 0 1px 3px rgba(0,0,0,.35);
}

/* The same book, in another language, on another shelf. Presented as what it
   is — a cross-reference, not a duplicate to be tidied away. */
.editions { margin: 0 0 16px; padding: 11px 13px; border-radius: var(--r-md);
  background: var(--surface-2); border: 1px solid var(--rule-soft); }
.editions .lead { display: block; font-size: 12px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin-bottom: 7px; }
.editions .edition {
  display: block; width: 100%; text-align: left; background: none; border: 0;
  padding: 4px 6px; margin: 0 -6px; border-radius: var(--r-sm);
  font-size: 13.5px; color: var(--ink); cursor: pointer;
}
.editions .edition:hover { background: var(--surface); }
.editions .edition .muted { font-size: 11.5px; margin-left: 6px; }
/* A box with nothing in it yet is only an invitation, so it loses the panel. */
.editions.empty { background: none; border: 0; padding: 0; margin: 0 0 14px; }
.editions .edition .unlink {
  float: right; color: var(--muted); font-size: 11px; padding: 0 4px; line-height: 1.5;
}
.editions .edition .unlink:hover { color: var(--accent); }
.editions .link-edition {
  display: block;   /* or it sits wherever the preceding line of text left off */
  margin-top: 8px; padding: 5px 10px; font-size: 12.5px;
  background: none; border: 1px dashed var(--rule); color: var(--muted);
}
.editions.empty .link-edition { margin-top: 0; }
.editions .link-edition:hover { color: var(--accent); border-color: var(--accent); background: none; }

/* the little library search that links one edition to another */
.link-hits { margin-top: 10px; max-height: 320px; overflow-y: auto;
  display: flex; flex-direction: column; gap: 2px; }
.link-hit {
  display: block; width: 100%; text-align: left; border: 0; background: none;
  padding: 7px 9px; border-radius: var(--r-sm); cursor: pointer;
}
.link-hit:hover { background: var(--surface-2); }
.link-hit b { display: block; font-family: var(--serif); font-size: 14px; font-weight: 650; }
.link-hit .muted { font-size: 12px; color: var(--muted); }
/* The lists you already keep, offered before the box for a new one — after
   making a list the commonest thing is adding to it. */
.pick-lists { display: flex; flex-direction: column; gap: 2px; max-height: 240px; overflow-y: auto; }

/* Saying which column of a spreadsheet is which. Four rows, because a table
   the app has never seen cannot be read by guessing alone — and because being
   shown the guess is what makes it safe to accept. */
.col-map { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 14px; margin-top: 12px; }
.col-map .field { margin: 0; }
@media (max-width: 700px) { .col-map { grid-template-columns: 1fr; } }

/* A detail of your own that carries a link. It sits in the details list beside
   Publisher, so it is a link first and a row of a table second. */
.sheet .fields .extra-link { color: var(--accent); text-decoration-thickness: 1px; }
.sheet .fields .extra-link:hover { text-decoration-thickness: 2px; }

/* ---------- the reading-log review ---------- */

/* Two columns per row: the line as it was written in the file, and the book it
   was matched to. Reading down either column tells you whether the matching is
   sane, which is the point of showing it at all. */
#review { width: min(980px, calc(100vw - 32px)); }
.group { font-family: var(--sans); font-size: 11px; font-weight: 650;
  letter-spacing: .07em; text-transform: uppercase; color: var(--muted);
  margin: 20px 0 8px; }
/* The instruction after a heading is a sentence, not more heading. Inheriting
   the uppercase ran it into the count — "NOT FOUND — 7 SEARCH FOR EACH". */
.group .small {
  text-transform: none; letter-spacing: 0; font-weight: 500; font-size: 11.5px;
  margin-left: 8px;
}
/* The proposal to make three shelves.
   One heading per shelf, one block per sort of thing on it, and a tick
   control on each block — because the useful answers here are "all of the
   books, none of the manuals", and making those two answers cost two taps
   instead of two hundred is the difference between a screen people read and
   one they press Apply on without reading. */
.az-shelf { margin-bottom: 22px; }
.az-group { margin: 0 0 14px; }
.az-head {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 6px 8px; font-size: 13px;
  border-bottom: 1px solid var(--rule-soft); margin-bottom: 4px;
}
.az-head button {
  background: none; border: 0; padding: 2px 4px; cursor: pointer;
  font: inherit; font-size: 12.5px; color: var(--accent);
}
.az-head button:hover, .az-head button:focus-visible { text-decoration: underline; }
/* Three columns, not four: nothing here is being matched *to* anything, so
   there is no arrow and no right-hand book. */
.az-row { grid-template-columns: 22px 1.2fr 1fr; }
.az-row .to .muted { display: inline; }
/* The offer at the foot of the reading-log review: turn what matched nothing
   into a shelf. Set apart from the list of misses above it, because it is the
   one thing on that part of the screen that is a proposal rather than a
   report. */
.elsewhere-offer {
  border-left: 3px solid var(--good); background: var(--surface-2);
  border-radius: 0 var(--r-md) var(--r-md) 0; padding: 14px 16px; margin-top: 18px;
}
.elsewhere-offer h4 { margin: 0 0 6px; font-family: var(--serif); font-size: 17px; }
.elsewhere-offer p { margin: 0 0 12px; }
.elsewhere-offer label { display: flex; align-items: center; gap: 9px; cursor: pointer; }

.az-notes {
  border-left: 3px solid var(--warn); background: var(--surface-2);
  border-radius: 0 var(--r-md) var(--r-md) 0; padding: 12px 14px; margin-top: 18px;
}
.az-notes p { margin: 0 0 8px; }
.az-notes p:last-child { margin-bottom: 0; }

.matches { display: flex; flex-direction: column; gap: 1px; }
.match {
  display: grid; grid-template-columns: 22px 1fr 16px 1fr; gap: 10px;
  align-items: baseline; padding: 7px 8px; border-radius: var(--r-md);
  font-size: 13px; cursor: pointer;
}
.match:hover { background: var(--surface-2); }
.match input { margin: 0; align-self: center; }
.match .arrow { color: var(--muted); }
.match .to { color: var(--ink); }
.match .to .muted { display: block; font-size: 11.5px; }
.match .why { font-size: 12px; color: var(--muted); font-style: italic; }
.match .yr { font-size: 12px; color: var(--muted); margin-left: 7px;
  font-variant-numeric: tabular-nums; }
.unmatched { max-height: 260px; overflow-y: auto; margin-top: 10px;
  padding: 10px; background: var(--surface-2); border-radius: var(--r-md); }

/* A line the matcher could not place, with a search of the library beside it.
   The grid is the match row's, minus the checkbox column — there is nothing to
   tick until a book has been chosen — so the two lists line up down the page
   and a row that gets answered slots into place without anything shifting. */
.unmatched-rows { margin-top: 4px; }
.match.unfound { grid-template-columns: 22px 1fr 16px 1fr; cursor: default; align-items: start; }
.match.unfound:hover { background: none; }
.match.unfound .from { grid-column: 2; padding-top: 7px; }
.match.unfound .arrow { padding-top: 7px; }
.match.unfound .to { display: block; }
.find-book { width: 100%; font-size: 13px; padding: 5px 9px; }
.find-hits { display: flex; flex-direction: column; gap: 1px; margin-top: 2px; }
.find-hits .link-hit { padding: 5px 8px; }
.find-hits .link-hit b { font-size: 13px; }
.find-hits .link-hit .muted { font-size: 11.5px; }
@media (max-width: 700px) {
  .match.unfound { grid-template-columns: 1fr; }
  .match.unfound .from { grid-column: 1; padding-top: 0; }
  .match.unfound .arrow { display: none; }
}
.unmatched div { padding: 1px 0; }

@media (max-width: 700px) {
  .match { grid-template-columns: 22px 1fr; }
  .match .arrow { display: none; }
  .match .to { grid-column: 2; padding-left: 12px; border-left: 2px solid var(--rule); }
}

/* The foot of the sidebar. Quiet enough to ignore, present enough to be a
   colophon rather than a footer — this is one person's library, and it says so
   once, at the bottom, in the smallest type in the app. */
.sidebar-foot { margin-top: auto; display: flex; flex-direction: column; gap: 10px; padding: 14px 4px 0; }
/* Centred, like the colophon under it — the foot of the column is one block,
   and half of it ranged left under a centred line read as a mistake. */
.sidebar-foot .sync-line { display: flex; align-items: center; justify-content: center; gap: 8px; }
.colophon {
  margin: 0; font-size: 12px; line-height: 1.5; color: var(--muted);
  border-top: 1px solid var(--panel-rule); padding-top: 10px;
  text-align: center;
}
/* About and privacy, first in the colophon: in lower case, like the rest of
   the house voice, and a size up from the © line, because they are the part
   of it somebody actually taps. */
.colophon-links { display: flex; justify-content: center; gap: 16px; margin-bottom: 4px; font-size: 13.5px; }
.colophon-links a { color: var(--ink-soft); font-weight: 600; text-decoration: none; padding: 2px 0; }
.colophon-links a:hover, .colophon-links a:focus-visible { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
/* "feedback" is a button (it opens a box, not a page) dressed as its two
   neighbours, with a carmine dot when an answer is waiting. */
.colophon-links .fb-link {
  font: inherit; font-weight: 600; color: var(--ink-soft); background: none; border: 0;
  padding: 2px 0; cursor: pointer; position: relative; min-height: 0; height: auto;
}
.colophon-links .fb-link:hover, .colophon-links .fb-link:focus-visible { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.fb-dot {
  position: absolute; top: 0; right: -8px; width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent);
}

/* ---------- feedback ---------- */

.feedback-dialog textarea { width: 100%; min-height: 8em; resize: vertical; font: inherit; line-height: 1.45; }
.fb-kinds { display: flex; flex-wrap: wrap; gap: 8px; }
.fb-shot-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.fb-shot-pick { cursor: pointer; }
.fb-shot-chosen { display: inline-flex; align-items: center; gap: 10px; }
.fb-shot-chosen img { height: 56px; width: auto; max-width: 120px; border-radius: 6px; border: 1px solid var(--rule); object-fit: cover; }
.fb-credit { display: flex; gap: 10px; align-items: flex-start; font-weight: 400; font-size: 14px; color: var(--ink-soft); }
.fb-credit input { width: auto; margin-top: 3px; flex: 0 0 auto; }
.fb-context { overflow-wrap: anywhere; }
.fb-actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }

.fb-list { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.fb-note { border: 1px solid var(--rule); border-radius: var(--r-md); padding: 12px 14px; background: var(--surface); }
.fb-note.unread { border-color: color-mix(in srgb, var(--accent) 45%, var(--rule)); }
.fb-head { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; flex-wrap: wrap; font-size: 13.5px; color: var(--ink-soft); }
.fb-body { margin: 6px 0 8px; white-space: pre-wrap; overflow-wrap: anywhere; }
.fb-facts { margin: 2px 0 0; overflow-wrap: anywhere; }
.fb-status { font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; padding: 2px 8px; border-radius: 999px; }
.fb-status.new { background: color-mix(in srgb, var(--accent) 14%, var(--surface)); color: var(--accent); }
.fb-status.seen { background: var(--surface-2); color: var(--ink-soft); }
.fb-status.fixed { background: color-mix(in srgb, var(--good) 16%, var(--surface)); color: var(--good); }
.fb-status.later { background: var(--surface-2); color: var(--muted); }
.fb-answer { display: grid; grid-template-columns: auto 1fr; gap: 8px; margin-top: 10px; align-items: start; }
.fb-update { grid-column: 1 / -1; display: flex; flex-direction: column; gap: 4px; font-weight: 400; }
.fb-update input { width: 100%; }
.fb-answer-actions { grid-column: 1 / -1; display: flex; gap: 8px; justify-content: flex-end; }
.fb-shot-view img { display: block; max-width: 100%; border-radius: 6px; border: 1px solid var(--rule); margin: 6px 0 8px; }
.fb-done summary { cursor: pointer; color: var(--ink-soft); padding: 6px 0; }
.fb-done .fb-note { margin-top: 10px; }

dialog.whatsnew ul { margin: 6px 0 12px; padding-left: 1.2em; }
dialog.whatsnew li { margin: 0 0 8px; line-height: 1.45; }

/* ---------- want to read / have read ---------- */

/* Two marks that have to be legible on a book's own page and nearly invisible
 * on the strip under the camera — the same control at two volumes.
 *
 * They are outlined until they are set, and filled when they are, because the
 * state has to be readable at a glance from arm's length while the phone is
 * pointed at a shelf. Want is the accent; read is the quieter green, which is
 * the right way round: the shelf you are building is the one you have not read.
 */
.read-marks { display: flex; gap: 7px; margin: 2px 0 16px; }
.read-marks .mark {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 13px; border-radius: 999px;
  border: 1px solid var(--rule); background: none; color: var(--ink-soft);
  font-size: 12.5px; font-weight: 500; cursor: pointer;
  transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.read-marks .mark .g { font-size: 13px; line-height: 1; }
.read-marks .mark:hover { border-color: var(--accent); color: var(--ink); }
.read-marks .m-want.on {
  background: var(--accent); border-color: transparent; color: var(--accent-ink);
}
.read-marks .m-read.on {
  /* --good is a light green in dark mode, so the label takes the same ink as
     the accent buttons rather than a hardcoded white that would vanish on it. */
  background: var(--good); border-color: transparent; color: var(--accent-ink);
}
.read-marks .mark:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The year, beside the tick.
   Shown by the state of its sibling rather than by a re-render: the marks
   paint themselves the instant they are pressed and the sheet is not drawn
   again, so a box that only exists when the record says "read" would not
   appear until you closed the page and opened it. */
.read-year { display: none; align-items: center; gap: 6px; margin-left: 4px; }
.read-marks:has(.m-read.on) .read-year { display: inline-flex; }
.read-year .ry-l { font-size: 12.5px; color: var(--muted); }
.read-year input {
  width: 4.6em; padding: 6px 9px; border-radius: 999px;
  border: 1px solid var(--rule); background: var(--surface); color: var(--ink);
  font: inherit; font-size: 12.5px; font-weight: 600; text-align: center;
  font-variant-numeric: tabular-nums;
}
.read-year input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.read-year input::placeholder { font-weight: 400; color: var(--muted); }

/* On the strip under the camera: glyph only, sized for a thumb, sitting on the
   tile rather than beside it so the strip stays one row of covers. */
.read-marks.compact { gap: 5px; margin: 5px 0 0; justify-content: center; }
.read-marks.compact .mark {
  /* The strip's tiles are 62px wide, so the two marks share that width rather
     than each claiming a comfortable one and pushing the row out of shape. */
  flex: 1 1 0; min-width: 0; padding: 5px 0; font-size: 11px; gap: 0;
  justify-content: center;
}
.read-marks.compact .mark .l { display: none; }
.read-marks.compact .mark .g { font-size: 12px; }

/* And on the wall, a mark small enough to be furniture. */
.tile .rmark {
  /* Bottom-right: the status flag owns the top-right corner and the spine
     shading runs down the left edge. */
  position: absolute; right: 7px; bottom: 7px; z-index: 2;
  width: 19px; height: 19px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; line-height: 1; color: #fff;
  box-shadow: 0 1px 3px rgba(0,0,0,.35);
}
.tile .rmark.want { background: var(--accent); }
.tile .rmark.read { background: var(--good); color: var(--accent-ink); }

/* Typing an ISBN: one field, big enough to check at a glance. */
dialog.ask { max-width: 420px; }
dialog.ask input { width: 100%; }
.small.bad { color: var(--warn, #b4453c); }

/* ---------- scanner ---------- */

/* width and the auto margins are deliberate. With `aspect-ratio` and a binding
   `max-height`, some browsers keep the ratio by narrowing the box instead of
   breaking it — and a narrowed block box with `width: auto` sits against its
   left edge, which is exactly how a centred camera window ends up looking
   shoved to one side. Stating the width and centring the margins makes the
   result the same either way. `dvh` keeps the cap honest on iOS, where `vh`
   is measured against the tallest the viewport ever gets, not what you see. */
.scan-stage {
  position: relative; background: #000; border-radius: var(--r-lg); overflow: hidden;
  aspect-ratio: 3/4; width: 100%; margin-inline: auto;
  max-height: 62vh; max-height: 62dvh;
}
.scan-stage video { width: 100%; height: 100%; object-fit: cover; display: block; }

.reticle { position: absolute; inset: 0; pointer-events: none; }
.reticle .band {
  position: absolute; left: 6%; right: 6%; top: 32%; height: 36%;
  border: 2px solid rgba(255,255,255,.85); border-radius: 10px;
  box-shadow: 0 0 0 2000px rgba(0,0,0,.42);
}
.reticle .laser {
  position: absolute; left: 8%; right: 8%; top: 50%; height: 2px;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  animation: sweep 2.2s ease-in-out infinite;
}
@keyframes sweep {
  0%, 100% { transform: translateY(-42px); opacity: .55; }
  50%      { transform: translateY(42px);  opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .reticle .laser { animation: none; }
  .tile:hover .art { transform: none; }
}

.flash { position: absolute; inset: 0; pointer-events: none; opacity: 0; }
.flash.on { animation: flashpop .42s ease-out; }
@keyframes flashpop { 0% { opacity: .85; } 100% { opacity: 0; } }

.scan-hud {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 12px 14px;
  display: flex; align-items: center; gap: 10px;
  background: linear-gradient(transparent, rgba(0,0,0,.72));
  color: #fff; font-size: 13px;
}
.scan-hud .tally { font-family: var(--serif); font-size: 27px; line-height: 1; font-variant-numeric: tabular-nums; }

.catch-strip { display: flex; gap: 10px; overflow-x: auto; padding: 14px 2px 6px; min-height: 104px; }
.catch {
  flex: 0 0 auto; width: 62px; display: flex; flex-direction: column; gap: 5px;
  animation: catchIn .3s cubic-bezier(.2,.8,.3,1);
}
@keyframes catchIn { from { opacity: 0; transform: translateY(14px) scale(.9); } }
.catch .art { width: 62px; height: 93px; border-radius: 2px; overflow: hidden; box-shadow: var(--shadow-book); background: var(--surface-2); }
.catch .art img { width: 100%; height: 100%; object-fit: cover; }
.catch .t { font-size: 11.5px; line-height: 1.25; color: var(--muted);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

/* ---------- book sheet ---------- */

/* One size, whatever is in it.
 *
 * The card used to be as tall as its contents, which is defensible for a book
 * you opened on purpose and wrong for a carousel: the Next button moved every
 * time, so using Showcase meant chasing it down the screen between books. A
 * fixed height puts the button in the same place for every card and lets the
 * body scroll when a description runs long — which is the right trade, because
 * the button is used on every card and the long descriptions are rare. */
dialog.sheet {
  border: none; padding: 0; background: var(--surface); color: var(--ink);
  border-radius: var(--r-lg); box-shadow: var(--shadow-2);
  width: min(760px, calc(100vw - 40px));
  height: min(660px, calc(100dvh - 48px));
  overflow: hidden;   /* so the rounded corners clip the sticky action bar */
}
/* The boxes that are a form rather than a card size themselves: a fixed height
   around one input is just empty room.

   `fit-content`, not `auto`, and the difference is the whole point. A modal
   dialog is given `inset: 0; margin: auto` by the browser's own stylesheet, so
   it is an absolutely-positioned box with a top AND a bottom — and on such a
   box `height: auto` means fill the space between them, not hug the content.
   This rule has said `auto` since it was written, which is why the box asking
   for one ISBN has always been six hundred pixels tall with a form adrift in
   the middle of it. */
dialog.sheet.ask { height: fit-content; max-height: min(660px, calc(100dvh - 48px)); }
/* Only when open, because `display` is what a dialog uses to show and hide. */
dialog.sheet[open] { display: flex; flex-direction: column; }
/* A box whose content is a form: the form is the dialog's child, not the body,
   so it has to pass the dialog's height on. Without this it grew to fit its
   content — a list of eight search results in Add by name is twice the height
   of the box — and the dialog, which clips its rounded corners, cut it off:
   the results were there and could not be scrolled to, in every browser
   (reported from Firefox and Edge, October 2026). Sized like this, the
   `.sheet-body` inside it is the part that scrolls, as everywhere else. */
dialog.sheet[open] > form { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
dialog.sheet::backdrop { background: rgba(20,18,16,.55); backdrop-filter: blur(3px); }
/* flex-basis stays `auto`, deliberately. With `flex: 1` the basis is 0, and in
   a flex column whose own height is content-based, Safari resolves that to a
   body of nothing — the sheet collapses to its cover and its buttons, which is
   exactly what happened on the phone. `0 1 auto` means: be as tall as your
   content, and shrink when the dialog runs out of room. */
.sheet-body { display: grid; grid-template-columns: 200px 1fr; gap: 26px; padding: 26px;
  overflow-y: auto; flex: 1 1 auto; min-height: 0; align-content: start; }
.sheet-cover { width: 200px; aspect-ratio: 2/3; border-radius: 2px 5px 5px 2px; overflow: hidden; box-shadow: var(--shadow-book); position: sticky; top: 0; }
.sheet-cover img { width: 100%; height: 100%; object-fit: cover; }
.sheet h2 { font-size: 25px; letter-spacing: -.015em; margin-bottom: 4px; }
.sheet .byline { font-size: 15px; color: var(--ink-soft); margin-bottom: 16px; }
.sheet .fields { display: grid; grid-template-columns: auto 1fr; gap: 7px 16px; font-size: 13.5px; margin: 16px 0; }
.sheet .fields dt { color: var(--muted); white-space: nowrap; }
.sheet .fields dd { margin: 0; }
.sheet .desc { font-family: var(--serif); font-size: 14.5px; line-height: 1.62; color: var(--ink-soft); margin: 16px 0; }
.sheet .close { position: absolute; top: 13px; right: 13px; }
.edit-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.edit-grid .full { grid-column: 1 / -1; }

/* one box per author, each with its own way out */
.author-rows { display: flex; flex-direction: column; gap: 6px; }
.author-row { display: flex; gap: 6px; align-items: center; }
.author-row input { flex: 1 1 auto; min-width: 0; }
.author-row button {
  flex: 0 0 auto; width: 34px; padding: 0; line-height: 1;
  color: var(--muted); font-size: 14px;
}
.author-row button:hover { color: var(--accent); }

/* ---------- chips, pills, misc ---------- */

.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  font-size: 13.5px; padding: 3.5px 11px; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--rule); color: var(--ink-soft);
}
.chip.on { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
/* A chip that is a door says so under the finger rather than at rest — the
   book's page is a thing to read, not a control panel. */
button.chip { cursor: pointer; font-family: inherit; }
button.chip:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--rule)); color: var(--accent); }
button.chip.on:hover { background: var(--accent-lo); color: var(--accent-ink); }

.pill-warn { background: var(--warn); color: #fff; border: none; }
.pill-good { background: var(--good); color: #fff; border: none; }

.toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; }
.spacer { flex: 1; }

/* Says one thing, once, and gets out of the way. Never a modal — the app is
   entirely usable in the state it is describing. */
.banner {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  background: var(--surface); border: 1px solid var(--rule);
  border-left: 3px solid var(--muted);
  border-radius: var(--r-md);
  padding: 12px 16px; margin-bottom: 18px;
  font-size: 15.5px; color: var(--ink-soft);
}
.banner.warn { border-left-color: var(--warn); }
.banner strong { color: var(--ink); }
.banner a { margin-left: auto; white-space: nowrap; font-weight: 550; }

.empty { text-align: center; padding: 70px 20px; color: var(--muted); }
.empty h3 { font-size: 21px; color: var(--ink); margin-bottom: 8px; }

.toast-host { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); z-index: 100;
  display: flex; flex-direction: column; gap: 8px; align-items: center; pointer-events: none; }
.toast {
  background: var(--ink); color: var(--paper); padding: 10px 18px; border-radius: 999px;
  font-size: 15px; box-shadow: var(--shadow-2); animation: toastIn .22s ease-out;
  max-width: min(92vw, 460px);
}
@keyframes toastIn { from { opacity: 0; transform: translateY(10px); } }

/* The toast host ignores pointer events so it never blocks the wall; the one
   thing inside it that can be tapped has to opt back in. */
.toast { display: flex; align-items: center; gap: 12px; }
.toast-action {
  pointer-events: auto;
  background: transparent; border: none; color: var(--paper);
  font-weight: 650; padding: 2px 2px; text-decoration: underline;
  text-underline-offset: 3px;
}
.toast-action:hover { background: transparent; opacity: .8; }

/* ---------- choosing several books ---------- */

button.danger { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, var(--rule)); }
button.danger:hover { background: color-mix(in srgb, var(--accent) 12%, var(--surface)); }

.selection-bar {
  position: fixed; z-index: 70;
  left: max(14px, var(--safe-left)); right: max(14px, var(--safe-right));
  bottom: calc(var(--tabbar) + var(--safe-bottom) + 14px);
  margin-inline: auto; max-width: 620px;
  /* Wraps as a last resort. The bar is designed to fit in one line and its
     two states are each short enough to, but a long library name is a string
     this app does not control — and a pill that overflows hides a button
     rather than showing a scrollbar. */
  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  padding: 9px 10px 9px 16px;
  background: var(--surface); border: 1px solid var(--rule);
  border-radius: 22px; box-shadow: var(--shadow-2);
  animation: toastIn .2s ease-out;
}
.selection-bar .count { font-size: 13.5px; font-weight: 550; white-space: nowrap; }
.selection-bar button { padding: 6px 12px; font-size: 13.5px; white-space: nowrap; }

/* The chosen state has to survive on a cover of any colour, so it is a ring
   plus a filled tick rather than a tint over the artwork. */
.tile .pick, .books .pick {
  position: absolute; top: 7px; left: 7px; width: 22px; height: 22px; z-index: 3;
  border-radius: 50%; border: 2px solid #fff; background: rgba(20,18,16,.35);
  box-shadow: 0 1px 4px rgba(0,0,0,.4);
}
.wall.selecting .tile .art { outline: 2px solid transparent; outline-offset: 2px; }
.tile.picked .art { outline: 2px solid var(--accent); outline-offset: 2px; }
.tile.picked .pick { background: var(--accent); border-color: var(--accent); }
.tile.picked .pick::after,
tr.picked .pick::after {
  content: '✓'; position: absolute; inset: 0; display: grid; place-items: center;
  color: var(--accent-ink); font-size: 13px; line-height: 1; font-weight: 700;
}
table.books td.c-pick { width: 34px; }
table.books .pick { position: relative; display: inline-block; top: 0; left: 0;
  border-color: var(--rule); background: var(--surface); }
tr.picked { background: color-mix(in srgb, var(--accent) 9%, transparent); }
tr.picked .pick { background: var(--accent); border-color: var(--accent); }

/* ---------- the sheet's actions ---------- */

.sheet-actions {
  position: sticky; bottom: 0; z-index: 2;
  display: flex; align-items: center; gap: 9px;
  padding: 12px 20px calc(12px + var(--safe-bottom));
  border-top: 1px solid var(--rule);
  background: color-mix(in srgb, var(--surface) 94%, transparent);
  backdrop-filter: blur(10px);
}

.progress { height: 4px; background: var(--surface-2); border-radius: 999px; overflow: hidden; }
.progress .fill { height: 100%; background: var(--accent); transition: width .25s ease; }

.spinner-ring {
  width: 15px; height: 15px; border: 2px solid var(--rule);
  border-top-color: var(--accent); border-radius: 50%;
  animation: spin .7s linear infinite; display: inline-block;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------- settings ---------- */

/* A 640px column in a 1500px window, set in thirteen-pixel grey.
 *
 * Every screen in this app is looked at; this one is *read*, and read in the
 * situations where reading is hardest — working out why a server will not
 * answer, or what a file about to be imported will do to three thousand
 * records. It was the smallest type in the app.
 *
 * So: bigger type throughout, a heading on each card you can find by
 * scanning rather than by reading, and two columns once there is room, which
 * there almost always is on a desk. Not three — a form column wider than
 * about sixty characters stops being a form and starts being a wall. */
.settings {
  max-width: 1180px;
  display: flex; flex-direction: column; gap: 22px;
  font-size: 15.5px;
}

/* Packed, not aligned. A grid would put "Appearance" — four words and a
   switch — in the same row as the Libib card and leave a hand's width of
   nothing under it. `columns` lets each card end where its content ends. */
/* 1160 and not 1040, because the sidebar is 268px of this window and the
   media query cannot see it: at 1040 the two columns are 365px each, which is
   narrower than the single column they replaced. */
@media (min-width: 1160px) {
  .settings { display: block; columns: 2; column-gap: 24px; }
  .settings > .card { break-inside: avoid; margin: 0 0 24px; }
  /* The build number belongs under both columns, not at the foot of one. */
  .settings .build { column-span: all; }
}

.settings .card {
  padding: 24px 26px;
  /* The app's own colour, at a strength that separates the cards from the
     ground without turning the screen into a set of warnings. */
  border-color: color-mix(in srgb, var(--accent) 26%, var(--rule));
}

/* The one line on each card you are meant to find with your eyes rather than
   read. Alegreya at 21px, in carmine, over a rule: the same voice the rest of
   the app uses for a heading, given the job of being a signpost. It is not
   the tiny uppercase label the Stats cards use — those sit above a number
   that is the content, and here the heading *is* the way in. */
.settings .card h3 {
  font-family: var(--serif); font-size: 21px; font-weight: 700;
  text-transform: none; letter-spacing: -.01em; line-height: 1.2;
  color: var(--accent);
  margin: 0 0 15px; padding-bottom: 11px;
  border-bottom: 1px solid color-mix(in srgb, var(--accent) 20%, var(--rule));
}

/* Everything that explains something, one step up from the size at which
   explanations stop being read. */
.settings p { line-height: 1.6; }
.settings .small { font-size: 14.5px; }
.settings .hint { font-size: 13.5px; line-height: 1.55; }
.settings label { font-size: 14.5px; }
.settings summary { font-size: 14.5px; }
.settings .diag-row { font-size: 14.5px; }
.settings .build { font-size: 13px; }

/* A section inside a card. Three files that each say something a catalogue
   cannot were three cards, which read as three subjects; they are one card
   with three headings now, and these are the headings. Quieter than the
   card's own — a rule above rather than below, so each one reads as the
   start of something rather than the label of the card. */
.settings .card h4 {
  font-family: var(--serif); font-size: 17px; font-weight: 700;
  letter-spacing: -.005em; color: var(--ink);
  margin: 26px 0 9px; padding-top: 16px;
  border-top: 1px solid var(--rule-soft);
}
.settings .card h4:first-of-type { margin-top: 4px; padding-top: 0; border-top: 0; }

/* A field with a button beside it, sharing the line. */
.row-fields { display: flex; gap: 9px; align-items: stretch; }
.row-fields input { flex: 1 1 auto; min-width: 0; }
.row-fields button { flex: 0 0 auto; }
.field { margin-bottom: 14px; }
.field:last-child { margin-bottom: 0; }
.hint { font-size: 14px; color: var(--muted); margin-top: 5px; line-height: 1.5; }

.diag-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--rule-soft); font-size: 13.5px; }
.diag-row:last-child { border-bottom: none; }
.dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 8px; }
.dot.ok { background: var(--good); }
.dot.bad { background: var(--accent); }
.dot.idle { background: var(--rule); }
.dot.warn { background: var(--warn); }

/* ---------- mobile ---------- */

@media (max-width: 860px) {
  /* The drawer's masthead is beside nothing, so the bar can be its own
     height here rather than the wordmark's. */
  :root { --tabbar: 68px; --topbar: 62px; --mark-h: 34px; }
  .app { flex-direction: column; }
  /* The drawer covers the whole screen height, so its masthead is under the
     island for the same reason the bar was. */
  .sidebar {
    position: fixed; inset: 0 auto 0 0; z-index: 60; width: 84vw; max-width: 320px;
    height: 100dvh; transform: translateX(-102%); transition: transform .22s cubic-bezier(.2,.8,.3,1);
    box-shadow: var(--shadow-2);
    padding-top: calc(20px + var(--safe-top));
    padding-bottom: calc(28px + var(--safe-bottom));
    /* Held in landscape — which is how you hold a phone at a bookshelf — the
       drawer's left edge is under the bezel on a notched phone, and 16px of
       padding is about 43px short of clearing it. The masthead bleeds to this
       same edge, so the two have to be one number. */
    --gutter: max(16px, var(--safe-left));
    padding-left: var(--gutter);
  }
  /* The drawer's masthead has no bar beside it to line up with, so it goes
     back to being sized by the name rather than by `--topbar` — at 62px the
     mark would be 34px tall in a column twice as wide as the desktop one. */
  .sidebar .brand {
    margin-top: calc(-20px - var(--safe-top));
    margin-left: calc(-1 * var(--gutter));
    padding: calc(18px + var(--safe-top)) 16px 14px var(--gutter);
    min-height: 0;
  }
  .sidebar .brand .mark { height: auto; width: 100%; }
  /* The table's pane has the tab bar under it here, and the safe area under
     that. Without this the last rows sit behind both. */
  .table-wrap {
    max-height: calc(100dvh - var(--topbar) - 4px - 44px
      - var(--tabbar) - var(--safe-bottom));
  }
  .sidebar.open { transform: none; }
  /* The wobble. Three things, because each closes a different door.
     `overscroll-behavior: none` stops the drawer's own rubber band and stops
     it handing a finished drag on to the page. `.fits`, set from the script
     when everything in the drawer is already on screen, takes away the
     scrolling itself — a column with nothing more to show should not move at
     all, and iOS will bounce a scroll container that is a pixel over. And the
     page behind is held still while the drawer is open, so a drag that
     starts on the scrim goes nowhere either. */
  .sidebar { overscroll-behavior: none; }
  .sidebar.fits { overflow-y: hidden; }
  html.drawer-open, html.drawer-open body { overflow: hidden; overscroll-behavior: none; }
  .scrim { touch-action: none; }
  .scrim { position: fixed; inset: 0; background: rgba(20,18,16,.5); z-index: 55; }
  /* The viewport is declared `viewport-fit=cover`, so on a notched iPhone the
     page really does extend under the rounded corners and the camera housing.
     Held in landscape — which is the natural way to hold a phone at a
     bookshelf — the inset is around 60px on one side, and content without
     these guards sits half under the bezel, reading as "shoved to the left".
     `max()` so the ordinary padding still applies where the inset is zero. */
  .content {
    padding: 15px max(14px, var(--safe-right))
             calc(var(--tabbar) + var(--safe-bottom) + 32px) max(14px, var(--safe-left));
  }

  /* The view controls do not fit beside a search field at 393px, so the bar
     becomes two rows and the controls scroll inside their own strip rather
     than pushing the whole page sideways. */
  /* The top inset is ADDED to the padding here, exactly as it is on the wide
     layout. Restating `padding` in this block and writing a flat 8px at the
     top is what put the search field and the hamburger under the dynamic
     island: the bar was told to start at the very top of a screen whose top
     59px are not the app's to draw on. */
  .topbar {
    height: auto; flex-wrap: wrap; row-gap: 8px;
    padding: calc(8px + var(--safe-top)) max(13px, var(--safe-right))
             8px max(13px, var(--safe-left));
  }
  .searchbox { min-width: 0; max-width: none; }
  #result-count { display: none; }
  /* Wrap, do not scroll. A strip that scrolls sideways hides controls behind
     an edge, and a fade only tells you something is hidden — it does not help
     you reach it one-handed. At 393px these five controls need two rows; two
     rows is what they get, and nothing ever falls off the edge. */
  #library-tools {
    order: 3; flex-basis: 100%; min-width: 0;
    flex-wrap: wrap; row-gap: 8px;
  }
  #library-tools > * { flex: 0 0 auto; max-width: 100%; }
  #library-tools select { padding: 6px 8px; font-size: 13.5px; }
  #library-tools button { padding: 6px 11px; font-size: 13.5px; }
  /* On a phone the four views are named by their shapes alone. Spelling them
     out costs a third row of chrome above the books, and the top of a phone
     screen is the most expensive space in the app — the island already takes
     the first 59px of it. The name stays in the title and the aria-label. */
  #view-picker button { padding: 7px 13px; font-size: 0; gap: 0; }
  #view-picker button .icon { font-size: 15px; opacity: 1; }


  /* height plus the reserved strip, not height including it. With
     box-sizing: border-box a 58px bar that reserves 34px for the home
     indicator leaves 24px for an icon and a label — so the buttons overflow
     their box and the bar's top border is drawn straight through them. */
  .tabbar {
    position: fixed; left: 0; right: 0; bottom: 0;
    height: auto; min-height: var(--tabbar);
    display: flex; z-index: 50;
    background: color-mix(in srgb, var(--surface) 94%, transparent);
    backdrop-filter: blur(14px) saturate(1.6);
    border-top: 1px solid var(--rule);
    padding: 0 var(--safe-right) var(--safe-bottom) var(--safe-left);
  }
  /* Bigger, because they were small for a thumb: 12px labels under 17px
     marks, on a bar a phone is mostly held by. The bar is ten pixels taller
     and every target in it is the full height of the bar. */
  .tabbar button {
    flex: 1; border: none; background: none; border-radius: 0;
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
    font-size: 13.5px; color: var(--ink-soft); font-weight: 600;
    min-height: var(--tabbar);
  }
  .tabbar button[aria-current="true"] { color: var(--accent); }
  .tabbar .icon { font-size: 22px; line-height: 1; }

  /* Add, as the thing the bar is for. A filled pine tile rather than one
     more word under one more mark: it is the only button here that does
     something rather than going somewhere, and on a phone at a bookshelf it
     is the one you are reaching for. */
  .tabbar .tab-add {
    flex: 1.15; margin: 7px 8px 7px 4px; min-height: calc(var(--tabbar) - 14px);
    border-radius: 16px; background: var(--good); color: var(--accent-ink);
    box-shadow: 0 2px 8px color-mix(in srgb, var(--good) 35%, transparent);
  }
  .tabbar .tab-add .icon { font-size: 26px; font-weight: 700; }
  .tabbar .tab-add[aria-current="true"] {
    color: var(--accent-ink);
    box-shadow: inset 0 0 0 2px var(--accent-ink), 0 2px 8px color-mix(in srgb, var(--good) 35%, transparent);
  }

  /* On a phone the three sizes have to be stated as column counts, not as
     minimum widths. At 393px a 97px minimum and an 88px minimum both come out
     at three columns — which is why Medium and Small used to look identical.
     Counting columns makes the difference exact at any phone width —
     minmax(0, …) rather than a bare 1fr, or a long unbroken Greek title sets
     a min-content floor under its column and the wall grows wider than the
     phone. */
  .wall       { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px 13px; }
  .wall.tight { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px 10px; }
  .wall.large { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 26px 18px; }
  .wall.tiny  { grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 10px 7px; }
  .sheet-body { grid-template-columns: 1fr; padding: 20px; }
  .sheet-cover { width: 150px; margin: 0 auto; position: static; }
  .edit-grid { grid-template-columns: 1fr; }
  .scan-stage { aspect-ratio: 3/4; max-height: 58vh; max-height: 58dvh; }
  table.books th { top: 0; }
}

@media (min-width: 861px) { .tabbar, .scrim, .menu-btn { display: none; } }

/* A tablet held upright is inside the phone's breakpoint, and so it got the
   phone's column counts: two, three and four across an 800-pixel screen,
   which made Small a 190-pixel cover — larger than Medium on a laptop. The
   counts are the right idea (they are what keeps sizes distinct), just one
   size down and one size further: Large here is what Small was, and
   Smallest is a shelf seen from across the room. */
@media (min-width: 600px) and (max-width: 860px) {
  .wall       { grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 22px 14px; }
  .wall.large { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 26px 18px; }
  .wall.tight { grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 16px 10px; }
  .wall.tiny  { grid-template-columns: repeat(10, minmax(0, 1fr)); gap: 12px 8px; }
  .wall       { --tile-type: 13.5px; }
  .wall.large { --tile-type: 15px; }
  .wall.tight { --tile-type: 11px; }
}
/* The same tablet on its side is past the phone breakpoint and gets the
   laptop's widths — on a screen a third smaller, held closer. One size down
   there too; a laptop is told apart by its pointer, not its width. */
@media (min-width: 861px) and (max-width: 1400px) and (pointer: coarse) {
  .wall       { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 22px 16px; }
  .wall.large { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 28px 20px; }
  .wall.tight { grid-template-columns: repeat(auto-fill, minmax(78px, 1fr)); gap: 16px 11px; }
  .wall.tiny  { grid-template-columns: repeat(auto-fill, minmax(58px, 1fr)); gap: 12px 8px; }
  .wall       { --tile-type: 13.5px; }
  .wall.large { --tile-type: 15px; }
  .wall.tight { --tile-type: 11px; }
}

@media print {
  .sidebar, .topbar, .tabbar, .toolbar { display: none !important; }
  .content { padding: 0; }
  .tile { break-inside: avoid; content-visibility: visible; }
}

/* The sheet body takes focus when a book opens (see openBook), which must not
   draw a ring — it is a scroll container, not a control. */
.sheet-body:focus { outline: none; }

/* ---------- the lock ---------- */

/* The front door. A page, not a dialog: the sidebar's apricot from edge to
   edge, the name as large as the screen allows with the masthead's carmine
   rule beneath it, the form sitting directly on the apricot in a pale panel
   of its own, and the colophon at the foot. Nothing behind it shows through
   — there is nothing behind it worth showing to someone who is not in. */
.lock {
  position: fixed; inset: 0; z-index: 200;
  display: flex; flex-direction: column; align-items: center;
  overflow-y: auto; overscroll-behavior: contain;
  background: var(--panel); color: var(--ink);
  padding: calc(env(safe-area-inset-top, 0px) + 6vh) max(20px, var(--safe-right))
           calc(env(safe-area-inset-bottom, 0px) + 22px) max(20px, var(--safe-left));
}
.lock.hidden { display: none; }
.lock-page {
  flex: 1 0 auto; width: min(520px, 100%);
  display: flex; flex-direction: column; align-items: stretch; gap: 30px;
}
.lock-brand {
  border-bottom: 4px solid var(--accent);
  padding-bottom: 18px;
}
.lock-brand .wordmark { margin: 0; }
.lock-brand .mark { display: block; width: 100%; height: auto; }
.lock-brand .mark .ink-pine { fill: var(--mark-pine); }
.lock-brand .mark .ink-carmine { fill: var(--mark-carmine); }

.lock-card {
  display: flex; flex-direction: column; gap: 14px; text-align: center;
  background: var(--on-panel); border: 1px solid var(--panel-rule);
  border-radius: var(--r-lg); padding: 26px 24px 20px;
}
.lock-card.hidden { display: none; }
/* The heading in the name's face: the one word on this page that is an
   invitation rather than an explanation. Latin only, as the face requires. */
.lock-card h2 {
  margin: 0; font-family: var(--logo); font-weight: 400; letter-spacing: .01em;
  font-size: 34px; line-height: 1.05; color: var(--accent);
}
.lock-card > p { margin: -4px 0 0; font-size: 15px; line-height: 1.55; color: var(--ink-soft); text-wrap: pretty; }
.lock-field { display: flex; flex-direction: column; align-items: stretch; gap: 6px; }
/* A sentence, not a form label: "Enter the email your invitation came to"
   is the page speaking, and set in small capitals it would shout. */
.lock-field label {
  margin: 0 0 2px; font-size: 16.5px; font-weight: 600; line-height: 1.35; color: var(--ink);
}
.lock-field input {
  width: 100%; background: var(--surface); text-align: center;
  border: 1.5px solid var(--panel-rule); border-radius: var(--r-md);
}
.lock-field input:focus-visible {
  outline: none; border-color: var(--slate);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--slate) 22%, transparent);
}
.lock-go { width: 100%; font-size: 22px; padding: 13px 20px 15px; margin-top: 2px; }
.lock-msg { margin: -4px 0 0; color: var(--accent); }
.lock-msg:empty { display: none; }
/* Nothing in the second row when nothing is offered there: no rule over an
   empty space. */
.lock-actions:not(:has(> :not([hidden]))) { display: none; }
.lock-card > p { margin-inline: auto; max-width: 46ch; text-wrap: balance; }
.lock-actions {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 22px;
  border-top: 1px solid var(--panel-rule); padding-top: 12px;
  font-size: 14px; color: var(--ink-soft);
}
.lock-actions .linkish:hover { color: var(--accent); text-decoration-color: var(--accent); }
.lock-foot { width: min(520px, 100%); margin-top: 36px; }
.lock-foot .colophon { border-top-color: var(--panel-rule); }

@media (max-width: 520px) {
  .lock-page { gap: 24px; }
  .lock-card { padding: 22px 18px 18px; }
  .lock-card h2 { font-size: 30px; }
}

/* The one field that wants to be bigger than the rest: it is the whole of
   what the screen is asking for. */
#sign-in-email, #shelf-name { font-size: 17px; padding: 13px 15px; }

/* Six digits, read off a phone and typed into a laptop. Set wide and
   monospaced so a 1 and a 7 and a 0 and an O are not a guessing game, and
   large enough to be checked against the mail at a glance. */
.code-field {
  font: 600 22px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: .34em; text-align: center;
}

/* Who is on a library. A row per person: the address they were invited at,
   what they may do, and — for the owner — the way to take them off again. */
.people { display: flex; flex-direction: column; gap: 2px; margin: 4px 0 12px; }
.person {
  display: flex; align-items: baseline; gap: 10px;
  padding: 9px 0; border-bottom: 1px solid var(--rule);
}
.person:last-child { border-bottom: 0; }
.person .who { flex: 1; min-width: 0; overflow-wrap: anywhere; }
/* The address under the name, because the name is the courtesy and the
   address is what actually tells two people apart. */
.person .addr { display: block; font-size: 12.5px; color: var(--ink-soft); }
.roster .person .addr + .addr { margin-top: 2px; }
.roster .seen { font-weight: 600; }
.roster .person-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 4px; flex: 0 1 auto; }
.roster .person-actions button { white-space: normal; text-align: right; }
.roster .seen.ok { color: var(--good); }
.roster .seen.wait { color: var(--warn, var(--accent)); }
.person .role {
  font-size: 11.5px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-soft);
}
.person .role.owner { color: var(--accent); }

/* A library crossing the wire onto a device that has none. */
.arriving-bar {
  width: min(320px, 100%); height: 6px; margin: 14px auto 16px;
  background: var(--surface-2); border-radius: 999px; overflow: hidden;
}
.arriving-bar span {
  display: block; height: 100%; background: var(--accent);
  border-radius: 999px; transition: width .4s ease;
}

/* A front camera's preview is mirrored, because a screen showing your own room
   should behave like a mirror — reaching a book towards an unmirrored preview
   means moving it the wrong way. Display only: the decoder reads the video
   element's own pixels, which a CSS transform does not touch. */
#cam.mirrored { transform: scaleX(-1); }

/* Deleting a library, at length.
   The only irreversible act in the app, and the only one that takes something
   away from other people too — so the dialog is allowed to be wordy, and the
   two choices about the books are given equal weight rather than one being a
   quiet link under the other.

   A class of its own rather than `.sheet`. A book's sheet is a fixed-height
   window with its own padded body, so borrowing it gave this dialog 660px of
   empty white below the text and no padding at all around it — the title ran
   off the corner and the first characters of every line were cut away by the
   rounded edge. It looked like a border problem and was a layout one. */
dialog.decide {
  border: none; background: var(--surface); color: var(--ink);
  border-radius: var(--r-lg); box-shadow: var(--shadow-2);
  width: min(520px, calc(100vw - 32px));
  height: fit-content;
  max-height: calc(100dvh - 48px);
  overflow: auto;
  padding: 26px 24px;
}
dialog.decide::backdrop { background: rgba(20,18,16,.55); backdrop-filter: blur(3px); }
dialog.decide h2 { font-size: 22px; letter-spacing: -.015em; margin: 0 0 8px; }
dialog.decide > form { display: flex; flex-direction: column; gap: 12px; }
dialog.decide p { margin: 0; }
.drop-choice {
  border: 1px solid var(--rule); border-radius: var(--r-lg);
  padding: 14px 16px; margin: 4px 0 6px;
}
.drop-choice legend {
  padding: 0 6px; color: var(--ink-soft);
  text-transform: uppercase; letter-spacing: .06em; font-size: 11.5px;
}
.drop-choice label {
  display: flex; gap: 10px; align-items: flex-start;
  margin: 0 0 12px; font-weight: 400; font-size: 14.5px; color: var(--ink);
}
.drop-choice label:last-child { margin-bottom: 0; }
.drop-choice input[type="radio"] { width: auto; margin-top: 3px; flex: 0 0 auto; }
#drop-name-field { margin: -4px 0 14px 26px; }
#drop-word { font-weight: 600; letter-spacing: .08em; }

/* The shelves you own, in Settings. Same row as the guest list, because it is
   the same kind of list: a thing, and what you may do to it. */
.shelves { display: flex; flex-direction: column; gap: 2px; margin: 4px 0 0; }
.shelf-row {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 0; border-bottom: 1px solid var(--rule);
}
.shelf-row:last-child { border-bottom: 0; }
.shelf-row .who { flex: 1; min-width: 0; overflow-wrap: anywhere; }

/* The library as something kept.
   Two series on one scale — arrivals in pine, departures in carmine — because
   two scales on one picture is two pictures pretending to be comparable.

   Both are tokens the theme already defines, not the hex values they hold in
   daylight. The first draft named a token that does not exist and gave it a
   literal green as a fallback — so every one of these would have painted the
   light theme's pine onto the dark theme's ground, in a stylesheet that
   redefines every other colour for dark mode. */
.keep-figures {
  display: flex; flex-wrap: wrap; gap: 22px 30px; margin: 0 0 18px;
}
.keep-figures > div { display: flex; flex-direction: column; gap: 2px; }
.keep-figures .fig {
  font: 400 30px/1 'Pattaya', Alegreya, Georgia, serif;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
.keep-figures .fig.in { color: var(--slate); }
.keep-figures .fig.out { color: var(--accent); }
.keep-figures .fig.quiet { color: var(--muted); }
.keep-figures .cap { font-size: 13px; letter-spacing: .04em; color: var(--ink-soft); }

.keep-chart .keep-pair {
  display: flex; align-items: flex-end; justify-content: center; gap: 2px;
  width: 100%; height: 100%;
}
.keep-chart .col { flex: 1 1 0; min-height: 0; max-width: 14px; }
.keep-chart .col.in { background: var(--slate); }
.keep-chart .col.out { background: var(--accent); }
.keep-chart .col:hover { filter: brightness(1.15); }

.keep-recent { margin-top: 16px; display: flex; flex-direction: column; }
.keep-row {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 10px; align-items: baseline;
  padding: 7px 0; border-bottom: 1px solid var(--rule); font-size: 13.5px;
}
.keep-row:last-child { border-bottom: 0; }
.keep-row .kind {
  font-size: 11.5px; letter-spacing: .07em; text-transform: uppercase;
  padding: 2px 6px; border-radius: 999px; color: var(--surface);
}
.keep-row .kind.in { background: var(--slate); }
.keep-row .kind.out { background: var(--accent); }
.keep-row .what { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.keep-row .when { color: var(--muted); font-size: 13px; white-space: nowrap; }

/* ---------- off the shelves ---------- */

/* A quieter register than the wall.
 *
 * The library is covers, shoulder to shoulder, because a shelf is a thing you
 * look at. This is a record, which is a thing you read: one book to a line, a
 * small jacket to recognise it by, and — carrying the weight of the page —
 * where it went and when. The two are deliberately not the same design.
 *
 * Carmine for the departure, which is the accent this app already uses for
 * things leaving: the "out" bars on the acquisitions chart are the same
 * colour, so the page and the chart are visibly about the same event. Lending
 * is the exception and is drawn in pine, because a lent book is expected
 * back — it is a different kind of absence and should not read as a loss. */
.gone-page { max-width: 940px; }
/* No heading of its own: the topbar already names the screen, and the app
   says each thing once. This is the sentence under that name. */
.gone-head { margin-bottom: 20px; }
.gone-head .lead { color: var(--ink-soft); font-size: 15px; max-width: 62ch; }

.gone-list {
  display: flex; flex-direction: column;
  border: 1px solid var(--rule); border-radius: var(--r-lg);
  background: var(--surface); overflow: hidden;
}
.gone-row {
  display: grid; grid-template-columns: 44px minmax(0, 1fr) auto;
  gap: 14px; align-items: center;
  padding: 12px 16px; text-align: left;
  background: none; border: 0; border-bottom: 1px solid var(--rule-soft);
  font: inherit; color: inherit; cursor: pointer;
}
.gone-row:last-child { border-bottom: 0; }
.gone-row:hover { background: var(--surface-2); }
.gone-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.gone-row .art {
  width: 44px; aspect-ratio: 2 / 3; border-radius: 2px 3px 3px 2px;
  overflow: hidden; background: var(--surface-2); box-shadow: var(--shadow-book);
}
.gone-row .art img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A generated binding is a title and an author stamped on a cloth, and at
   44px wide neither of them is legible — it reads as a smudge that looks like
   a rendering fault. The cloth alone, which is what the table's thumbnails
   do for the same reason. */
.gone-row .art .b-title, .gone-row .art .b-author { display: none; }
.gone-row .art .binding { padding: 0; }
.gone-row .who { min-width: 0; }
.gone-row .who .t {
  font-family: var(--serif); font-size: 16px; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gone-row .who .a {
  color: var(--muted); font-size: 14px; margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gone-row .small-chip { display: inline-block; margin-top: 5px; font-size: 11px; }

.gone-row .went { text-align: right; display: flex; flex-direction: column; gap: 2px; }
.gone-row .went .why { font-size: 13.5px; font-weight: 550; color: var(--accent); }
/* The two that are not departures at all: a book on loan is coming back and a
   book in the giveaway pile is still in the flat. Carmine is for "gone". */
.gone-row .went .why-lent { color: var(--slate); }
.gone-row .went .why-togive { color: var(--good); }

/* The three destinations, as three lists.
   One list ordered by date put a book lent last week beside one sold in 2019
   and said nothing about the difference — which is the only thing this screen
   is ever asked. */
.gone-group { margin-bottom: 26px; }
.gone-group h3 {
  font-family: var(--serif); font-size: 21px; font-weight: 700; margin: 0;
  display: flex; align-items: baseline; gap: 10px;
}
.gone-group h3 .n {
  font-family: var(--sans); font-size: 14px; font-weight: 600; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.gone-group > .small { margin: 2px 0 12px; }
.gone-row .went .when { font-size: 13px; color: var(--muted); }
.gone-row .went .note { font-size: 13px; color: var(--ink-soft); font-style: italic; }
.gone-foot { margin-top: 14px; }

/* "Where to?" — the second tap on Remove.
   It takes the place of the button it replaces rather than opening anything,
   so the footer keeps its shape and the answer is one more tap in the same
   part of the screen your hand is already in. */
.where-to { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.where-to .wt-q { font-size: 13px; color: var(--muted); margin-right: 2px; }
.where-to button {
  padding: 6px 11px; font-size: 13px; border-radius: 999px;
  border: 1px solid var(--rule); background: var(--surface); color: var(--ink);
  font: inherit; font-size: 13px; cursor: pointer;
}
.where-to button:hover, .where-to button:focus-visible {
  border-color: var(--accent); color: var(--accent);
}

/* The departure on a book's own page.
 *
 * Set on the surface behind it rather than in a card of its own: it is a fact
 * about this book, sitting where the book's other facts sit, and a bordered
 * box here would read as a warning. The left rule carries the colour instead,
 * which is the same device the banner uses for the same reason. */
.departed {
  border-left: 3px solid var(--accent);
  background: var(--surface-2);
  border-radius: 0 var(--r-md) var(--r-md) 0;
  padding: 12px 14px; margin: 0 0 16px;
}
.departed-when { font-size: 13.5px; font-weight: 600; color: var(--accent); }
.departed-form { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.departed-form select { flex: 0 0 auto; }
.departed-form input { flex: 1 1 160px; min-width: 0; }

/* Labels that exist for a screen reader and not for the eye. The controls
   above are self-evident in place — a menu of reasons beside a box asking who
   has it — and a printed label on each would say twice what the chip above
   already says once. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

@media (max-width: 620px) {
  /* The date and the destination move under the title rather than being
     squeezed beside it: "Given to Αλεξάνδρα" wrapped to three lines in a
     column 70px wide is unreadable, and a shelf of them is a wall of noise. */
  .gone-row { grid-template-columns: 40px minmax(0, 1fr); }
  .gone-row .art { width: 40px; }
  .gone-row .went { grid-column: 2; text-align: left; flex-direction: row; gap: 8px; flex-wrap: wrap; }
}

/* The departures, among the lists.
 *
 * Same row as "42 still need naming" and the collections, because that is
 * what it is — a list of books, just not of books that are here. The arrow is
 * the only difference and it carries the only difference that matters: every
 * other row narrows what you are looking at, this one takes you somewhere
 * else. */
.ov-gone .away { margin-left: 4px; opacity: .55; font-size: 11px; }

/* The "out" figure on the Stats ledger, once it is a door. It has to keep the
   shape of the three figures beside it exactly — a button that is a pixel
   taller than the numbers it sits with turns a row of statistics into a row
   of statistics and one control. */
.keep-door {
  display: flex; flex-direction: column; gap: 2px;
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; text-align: left; cursor: pointer;
}
.keep-door:hover .fig.out { color: var(--accent-lo); text-decoration: underline; }
.keep-door:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }

/* The answer to a search that found nothing on the shelves. The list inside
   is the same list as the departures screen, so it is the same component;
   only the frame around it changes. */
.not-here { padding: 46px 0 20px; text-align: left; }
.not-here h3 { margin-bottom: 6px; }
.not-here > p { max-width: 62ch; margin-bottom: 18px; }
/* The same width the departures screen gives the same list. Left to run the
   full width of a desktop window, "Lost · Αύγουστος 2026" ends up half a
   metre from the title it belongs to. */
.not-here .gone-list { text-align: left; max-width: 940px; }

/* ---------- lists, from a book's own page ---------- */

/* The panel under the list chips on a book's page.
 *
 * Inset and on the second surface, so it reads as a drawer that opened rather
 * than as another block of the page: it is a control that is either there or
 * not, and the chips above it are the thing it changes. */
.list-panel {
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  padding: 12px 13px;
  margin: 0 0 16px;
}
.list-rows { display: flex; flex-direction: column; gap: 4px; }
.list-row { display: flex; align-items: center; gap: 8px; }

/* The whole row is the target, not a 14px checkbox: this gets used standing
   at a shelf with a phone in one hand. */
.list-row .tick {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 9px;
  background: none; border: 1px solid transparent; border-radius: var(--r-sm);
  padding: 6px 8px; font: inherit; font-size: 14.5px; color: var(--ink);
  text-align: left; cursor: pointer;
}
.list-row .tick .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-row .tick .box {
  flex: 0 0 auto;
  width: 19px; height: 19px; border-radius: 5px;
  border: 1px solid var(--rule); background: var(--surface);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px; line-height: 1; color: transparent;
}
.list-row.on .tick .box {
  background: var(--accent); border-color: transparent; color: var(--accent-ink);
}
.list-row .tick:hover { background: var(--surface); border-color: var(--rule); }
.list-row .tick:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The month, beside the list it belongs to. Small and optional-looking,
   because it is optional. */
.list-row .when {
  flex: 0 0 96px; width: 96px;
  padding: 5px 9px; font-size: 13px; border-radius: var(--r-sm);
  background: var(--surface); border: 1px solid var(--rule); color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.list-new { display: flex; gap: 8px; margin-top: 10px; }
.list-new input {
  flex: 1 1 auto; min-width: 0;
  padding: 8px 11px; font-size: 14.5px; border-radius: var(--r-md);
  background: var(--surface); border: 1px solid var(--rule); color: var(--ink);
}
.list-new button { flex: 0 0 auto; }
.list-panel .hint { margin-top: 9px; }

@media (max-width: 520px) {
  /* The date drops under its list rather than squeezing the name to nothing. */
  .list-row { flex-wrap: wrap; }
  .list-row .when { flex: 0 0 auto; margin-left: 36px; }
}

/* The rename field inside a library's row.
 *
 * Hidden until asked for, because a name that is always a text box is a name
 * somebody will change by leaning on the keyboard. It replaces the row's own
 * contents rather than sitting under them, so the row does not jump. */
.shelf-row { position: relative; flex-wrap: wrap; }
.shelf-row .rename-row { display: none; width: 100%; gap: 8px; align-items: center; }
.shelf-row.renaming .rename-row { display: flex; }
.shelf-row.renaming .who,
.shelf-row.renaming [data-rename],
.shelf-row.renaming [data-drop] { display: none; }
.shelf-row .rename-row input {
  flex: 1 1 auto; min-width: 0;
  padding: 8px 12px; font-size: 15.5px; border-radius: var(--r-md);
  background: var(--surface); border: 1px solid var(--rule); color: var(--ink);
}
/* The line that used to be inside a browser dialog, where it was read by
   nobody, now under the button it is about. */
.shelf-row .taking-off { flex: 1 1 100%; margin-top: 6px; }

/* ---------- the book's own bar ---------- */

/* Done, and the thing that is not beside it.
 *
 * Remove was bottom-right, filled carmine, in the corner every application on
 * earth reserves for Done — and this card had no Done, and did not close on a
 * click outside, so the only way out was a small ✕ in the opposite corner. A
 * cursor looking for the exit went to the delete button. */
.sheet-actions .quiet-danger {
  margin-right: auto;
  background: none; border-color: transparent;
  color: color-mix(in srgb, var(--accent) 72%, var(--muted));
  font-size: 14px; padding: 6px 10px;
}
.sheet-actions .quiet-danger:hover {
  background: none; color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 45%, var(--rule));
}

/* The onward action — Next book in Showcase, Put back on a book that has
   left. Pine rather than carmine, and in the app's own face where the word is
   Latin: it is the same kind of button as Showcase itself, which is what
   started the sequence it continues. */
.sheet-actions .onward {
  font-family: var(--logo); font-size: 20px; line-height: 1;
  padding: 7px 18px 9px;
  background: var(--slate); color: var(--accent-ink); border-color: transparent;
}
.sheet-actions .onward:hover { background: color-mix(in srgb, var(--slate) 82%, black); }

/* Something the app knows and you do not.
 *
 * Used where a screen has to report a state that is not an error and is not
 * fine either — the server answering with an older parser than this device
 * reads with, which breaks nothing and quietly makes a repair pointless. It
 * reads as a note rather than an alarm, and it is a note nobody can miss. */
.warn-note {
  background: color-mix(in srgb, var(--warn) 12%, var(--surface));
  border-left: 3px solid var(--warn);
  color: var(--ink);
  padding: 11px 14px;
  border-radius: 0 var(--r-sm, 8px) var(--r-sm, 8px) 0;
}

/* The version the server is serving, when it is not the one running here. */
.build .stale { color: var(--warn); }

/* ==========================================================================
   The statistics, as a portrait rather than a grid of cards.

   Fourteen equal cards said that every fact about a library is worth the same
   amount of attention, which is not true of any library. What replaces them is
   a hero that states the place in a sentence, two figures about the reader
   rather than the books, and then the evidence in strips whose left-hand
   column says what each one is — so reading down that column tells you what
   the page knows before you have read a single number.
   ========================================================================== */

.stats-page { max-width: 1080px; }

.stat-hero {
  background: var(--apricot);
  color: var(--on-apricot);
  border-radius: 18px;
  padding: 34px 34px 26px;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 34px;
  align-items: end;
}
/* A `1fr` track's automatic minimum is min-content, and a row of figures whose
   captions do not wrap has a large one — which pushes the whole column past
   the window. This is the fix every grid needs and this one needed twice. */
.stat-hero > * { min-width: 0; }

.stat-hero .portrait {
  font-family: var(--serif);
  font-size: clamp(21px, 2.5vw, 28px);
  line-height: 1.42;
  max-width: 34ch;
  margin: 0;
}
.stat-hero .portrait em {
  font-style: normal; font-weight: 700; color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.stat-count {
  font-family: var(--logo); font-weight: 400; line-height: .9;
  color: var(--accent); font-size: clamp(56px, 9vw, 104px);
  font-variant-numeric: tabular-nums; text-align: right;
}
.stat-count small {
  display: block; font-family: var(--sans); font-size: 15px;
  color: color-mix(in srgb, var(--on-apricot) 70%, transparent);
  line-height: 1.3; margin-top: 9px;
}
.herofigs {
  grid-column: 1 / -1; display: flex; gap: 30px; flex-wrap: wrap;
  border-top: 1px solid color-mix(in srgb, var(--accent) 26%, transparent);
  padding-top: 18px;
}
.herofigs .hf {
  display: flex; flex-direction: column; min-width: 0; text-align: left;
  background: none; border: 0; padding: 0; font: inherit; color: inherit;
}
button.hf { cursor: pointer; }
button.hf:hover b, button.hf:focus-visible b { color: var(--accent); }
.herofigs b {
  font-family: var(--serif); font-size: 24px; font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.herofigs span {
  font-size: 15px; max-width: 20ch;
  color: color-mix(in srgb, var(--on-apricot) 70%, transparent);
}

/* The two facts about the reader. No heading: the numbers are the heading. */
.reading-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 15px; }
.reading-pair .rp {
  display: block; text-align: left; cursor: pointer; font: inherit;
  border-radius: 16px; padding: 26px 28px 24px; border: 1px solid var(--rule);
}
.rp-n {
  font-family: var(--logo); font-weight: 400; display: block;
  font-size: clamp(44px, 7vw, 74px); line-height: .92; font-variant-numeric: tabular-nums;
}
.rp-w { font-family: var(--serif); font-size: 23px; font-weight: 700; display: block; margin-top: 7px; }
.rp-s { font-size: 16.5px; color: var(--ink-soft); display: block; margin-top: 9px; max-width: 42ch; }
.rp-s b { font-weight: 600; color: var(--ink); }
.reading-pair .have {
  background: color-mix(in srgb, var(--good) 11%, var(--surface));
  border-color: color-mix(in srgb, var(--good) 32%, transparent);
}
.reading-pair .have .rp-n, .reading-pair .have .rp-w { color: var(--good); }
.reading-pair .next {
  background: color-mix(in srgb, var(--accent) 9%, var(--surface));
  border-color: color-mix(in srgb, var(--accent) 30%, transparent);
}
.reading-pair .next .rp-n, .reading-pair .next .rp-w { color: var(--accent); }
.reading-pair .rp:hover { box-shadow: var(--shadow-1); }

/* The way to send the three boxes above. Under them and to the right, where
   the eye arrives after reading them, and quiet: it is offered, not urged. */
.stats-share { display: flex; justify-content: flex-end; margin-top: 14px; }
.share-card .share-preview {
  display: block; width: 100%; max-width: 520px; aspect-ratio: 1; margin: 0 auto;
  border-radius: 12px; box-shadow: var(--shadow-1); border: 1px solid var(--rule);
}

.evidence { margin-top: 30px; }
.strip {
  display: grid; grid-template-columns: 210px 1fr; gap: 24px; align-items: start;
  padding: 24px 0; border-top: 1px solid var(--rule-soft);
}
.strip > * { min-width: 0; }
.strip:first-child { border-top: 0; padding-top: 4px; }
.strip-lab h3 {
  font-family: var(--serif); font-size: 21px; margin: 0 0 3px;
  font-weight: 700; line-height: 1.25;
}
.strip-lab p { margin: 0; font-size: 15.5px; color: var(--muted); }
.strip-lab p.cov { color: var(--warn); }
.strip-body .card { padding: 22px 24px; }

.statbars { display: flex; flex-direction: column; gap: 10px; }
/* `[hidden]` is only `display: none` because the user-agent stylesheet says
   so, and any `display` of ours outranks it. Rows eleven to twenty are in the
   page and hidden, so this one line is the difference between a list that
   opens and a list that was always open. */
.sb-row[hidden] { display: none; }
.sb-head { display: flex; justify-content: space-between; gap: 14px; align-items: baseline; font-size: 17px; }
/* The bar and its figure share one line, and the figure begins where the bar
   stops. The track is only as wide as the longest bar is allowed to be, so
   the groove ends where the scale ends rather than running on past every bar
   in the chart to a hundred per cent nothing reaches. */
.sb-meter { position: relative; height: 21px; margin-top: 2px; }
.sb-n {
  position: absolute; top: 0; line-height: 21px; margin-left: 9px; white-space: nowrap;
  font-variant-numeric: tabular-nums; color: var(--ink-soft); font-weight: 600; font-size: 15px;
}
.sb-label {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink);
  background: none; border: 0; padding: 0; font: inherit; text-align: left;
}
button.sb-label {
  cursor: pointer; text-decoration: underline; text-decoration-color: transparent;
  text-underline-offset: 3px; transition: text-decoration-color .12s, color .12s;
}
button.sb-label:hover, button.sb-label:focus-visible {
  color: var(--accent); text-decoration-color: currentColor;
}
.sb-track {
  position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  height: 7px; background: var(--surface-2); border-radius: 4px; overflow: hidden;
}
.sb-fill { height: 100%; background: var(--accent); border-radius: 4px; }
.sb-more { display: flex; justify-content: flex-end; margin-top: 12px; }
.sb-more button {
  background: none; border: 0; padding: 4px 2px; cursor: pointer;
  font: inherit; font-size: 15.5px; font-weight: 600; color: var(--accent);
}
.sb-more button:hover, .sb-more button:focus-visible { text-decoration: underline; text-underline-offset: 3px; }

/* A chart with a number over every bar needs room for the numbers, and
   twenty-three of them do not fit a phone — so the chart, and only the chart,
   scrolls in its own box rather than pushing the page sideways. */
.chart-pane { overflow-x: auto; overscroll-behavior-x: contain; padding-bottom: 4px; }
.statchart { display: flex; align-items: stretch; gap: 6px; height: 210px; min-width: 100%; }
/* Two fixed rows to a column — the bar's room, and the label. A column whose
   tick is blank used to be one row shorter than its neighbours, so every
   other year sat lower than the rest, which reads as a rendering fault and
   was one. */
.sc-slot { flex: 1 1 26px; min-width: 26px; display: flex; flex-direction: column; gap: 6px; }
/* The figure rides on its own bar: positioned from the bottom of the room by
   exactly the height the bar was given, so it lifts and falls with it. The
   scale stops at 85% of the room (REACH in views.js), which is what keeps the
   tallest column's number inside the chart. */
.sc-v {
  position: absolute; left: -4px; right: -4px; bottom: 0; margin-bottom: 4px;
  height: 17px; line-height: 17px; font-size: 13.5px; font-weight: 600;
  color: var(--ink-soft); text-align: center; font-variant-numeric: tabular-nums;
  pointer-events: none;
}
/* The bar is positioned inside its room rather than flexed in it. A percentage
   height against a box that is still deciding its own height is undefined, and
   the browser settles it by shrinking the tall bars to fit — which brought the
   two ends of the scale together: 906 and 1.181 came out the same height. */
.sc-room { flex: 1 1 auto; min-height: 0; position: relative; }
.sc-col {
  position: absolute; left: 0; right: 0; bottom: 0; min-height: 3px;
  background: var(--accent); border: 0; padding: 0; border-radius: 3px 3px 0 0;
}
button.sc-col { cursor: pointer; }
button.sc-col:hover, button.sc-col:focus-visible { background: var(--accent-lo); }
.statchart.pine .sc-col { background: var(--good); }
.statchart.pine button.sc-col:hover { background: var(--accent); }
.sc-tick {
  flex: none; height: 20px; line-height: 20px; font-size: 13.5px;
  color: var(--muted); text-align: center; font-variant-numeric: tabular-nums;
}

.stat-plain { font-size: 17px; color: var(--ink); margin: 0; line-height: 1.8; }
.stats-page .unknown { background: var(--surface-2); border-radius: 14px; padding: 22px 24px; }
.stats-page .unknown dl {
  margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 10px 18px; font-size: 16.5px;
}
.stats-page .unknown dt { font-weight: 700; font-family: var(--serif); }
.stats-page .unknown dd { margin: 0; color: var(--ink-soft); }

@media (max-width: 820px) {
  .stat-hero { grid-template-columns: 1fr; gap: 20px; padding: 26px 22px 22px; }
  .stat-count { text-align: left; }
  .stat-hero .portrait { max-width: none; }
  .strip { grid-template-columns: 1fr; gap: 12px; }
  .reading-pair { grid-template-columns: 1fr; }
}

/* The Stats page with a filter that matches nothing.
   Deliberately not the wall's empty state: two screens drawing the same four
   words is how the Stats button came to look broken. */
.stats-quiet {
  background: var(--surface-2); border-radius: 16px;
  padding: 30px 32px; max-width: 54ch;
}
.stats-quiet h3 { font-family: var(--serif); font-size: 23px; margin: 0 0 8px; font-weight: 700; }
.stats-quiet p { margin: 0; font-size: 17px; color: var(--ink-soft); }

/* Drawing the founding line. A proposal in the body of a card, so it reads as
   a sentence you can press rather than as a control bolted to the corner. */
.draw-line {
  background: none; border: 1px solid var(--good); color: var(--good);
  border-radius: 999px; padding: 7px 14px; cursor: pointer;
  font: inherit; font-size: 14.5px; font-weight: 600;
}
.draw-line:hover, .draw-line:focus-visible { background: var(--good); color: var(--accent-ink); }

/* Adding a book by name: the candidates, each with what it would bring. */
.bn-results { margin-top: 16px; display: flex; flex-direction: column; gap: 2px; }
.bn-row {
  display: grid; grid-template-columns: 40px minmax(0, 1fr) auto; gap: 12px;
  align-items: center; padding: 8px; border-radius: var(--r-md);
}
.bn-row:hover { background: var(--surface-2); }
.bn-art { width: 40px; aspect-ratio: 2 / 3; border-radius: 2px; overflow: hidden; background: var(--surface-2); }
.bn-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.bn-who { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.bn-who b { font-family: var(--serif); font-size: 16px; font-weight: 600; }
.bn-typed { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--rule-soft); }
.bn-typed p { margin: 0 0 10px; }
.bn-typed .bn-as { font-family: var(--serif); font-size: 15.5px; color: var(--ink); font-style: italic; }

/* The foot of the two boxes that add a book: the Add screen's own buttons,
   and Cancel as a ghost the same height beside them. */
.ask-actions { display: flex; align-items: center; gap: 10px; margin-top: 8px; flex-wrap: wrap; }
.ask-actions .ghost { padding: 11px 16px; }
.bn-match .to { display: flex; flex-direction: column; gap: 2px; }
.bn-thumb { width: 28px; aspect-ratio: 2 / 3; object-fit: cover; border-radius: 2px; float: left; margin-right: 8px; }

/* ---------- the first screen of a new library ---------- */

.welcome { max-width: 720px; margin: 8px auto 40px; }
.welcome-kicker {
  margin: 0 0 6px; font-family: var(--logo); font-size: 22px; color: var(--accent);
}
.welcome h2 { margin: 0 0 12px; font-family: var(--serif); font-size: clamp(26px, 4vw, 34px);
  line-height: 1.15; text-wrap: balance; }
.welcome h3 { margin: 30px 0 8px; font-family: var(--serif); font-size: 21px; }
.welcome p { font-size: 16.5px; line-height: 1.6; color: var(--ink-soft); max-width: 62ch; }
.welcome .welcome-lead { color: var(--ink); }
.welcome-ways { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-top: 18px; }
.welcome-way {
  display: flex; flex-direction: column; gap: 6px; align-items: flex-start; text-align: left;
  padding: 16px 16px 18px; border-radius: var(--r-lg);
  border: 1px solid color-mix(in srgb, var(--slate) 30%, var(--rule)); background: var(--surface);
}
.welcome-way b { font-family: var(--logo); font-weight: 400; font-size: 22px; color: var(--slate); }
.welcome-way span { font-size: 14.5px; line-height: 1.45; color: var(--ink-soft); }
.welcome-way:hover { border-color: var(--slate); }
.welcome-way.go { background: var(--accent); border-color: transparent; }
.welcome-way.go b, .welcome-way.go span { color: var(--accent-ink); }
.welcome-way.go:hover { background: var(--accent-lo); }
.welcome .linkish { color: var(--accent); font-weight: 600; text-decoration-color: currentColor; }
.welcome-then { display: grid; grid-template-columns: max-content 1fr; gap: 10px 18px; margin: 4px 0 0; }
.welcome-then dt { font-weight: 700; color: var(--ink); font-size: 15.5px; }
.welcome-then dd { margin: 0; font-size: 15.5px; line-height: 1.5; color: var(--ink-soft); }
@media (max-width: 640px) {
  .welcome-ways { grid-template-columns: 1fr; }
  .welcome-then { grid-template-columns: 1fr; gap: 2px; }
  .welcome-then dd { margin-bottom: 10px; }
}
