/* --------------------------------------------------------------------
   base.css — tokens, reset, and the chrome shared by both screens.

   Colour lives entirely in themes.css. Everything here refers to variables,
   which is what lets a theme switch repaint the whole app without touching
   a single rule below.

   One structural rule: no motion is defined outside the `@media
   (prefers-reduced-motion: no-preference)` guard and the `[data-epaper]`
   override at the bottom of this file. E-paper mode has to be able to switch
   all of it off in one place.
   -------------------------------------------------------------------- */

:root {
  /*
   * The typeface, the type scale and the motion durations live in tokens.css.
   *
   * They were declared here too, and because this file is linked *after*
   * tokens.css these copies silently won — so the interface went on being set
   * in the system sans while the bundled book face sat there unused, loaded
   * and never asked for. A duplicated token is not a harmless duplicate; it is
   * whichever one happens to be later in the document, which is not a decision
   * anybody made.
   */

  --sp-1: 0.25rem;
  --sp-2: 0.5rem;
  --sp-3: 0.75rem;
  --sp-4: 1rem;
  --sp-5: 1.5rem;
  --sp-6: 2rem;

  --radius: 8px;
  --radius-sm: 5px;
  --tap: 34px;

  --bar-h: 52px;
  --rail-w: 190px;
  --panel-w: 320px;
  --panel-w-wide: 380px;
  /* The menu drawer and the side panels it opens — highlights, vocabulary,
     practice, the author — share one width. See `.modal-drawer`. */
  --drawer-w: min(520px, 100vw);

  /* --speed and --ease are in tokens.css, beside the burn cycle and the
     flicker, so every duration in the product is described in one place. */

  /*
   * Aliases for names the language layer was written against.
   *
   * `language.css` styles the word card, the vocabulary list and the practice
   * rounds using `--surface`, `--ink`, `--line` and `--shadow-lg` — and none of
   * those had ever been defined anywhere. An undefined custom property makes
   * the whole declaration invalid, so the word card had **no background, no
   * border and no shadow**: it rendered as floating text with the book showing
   * straight through it, which is exactly how it was reported.
   *
   * Aliased rather than renamed across the file, because these resolve against
   * the themed tokens on the same element and so follow all eighteen themes
   * for free. `--focus` and `--rule` are here for the same reason.
   */
  --surface: var(--bg-2);
  --surface-2: var(--bg-3);
  --ink: var(--fg);
  --line: var(--border);
  --rule: var(--border);
  --focus: var(--accent);
  --shadow-lg: var(--shadow);
}

/* ---------------------------------------------------------------- */
/* Reset                                                             */
/* ---------------------------------------------------------------- */

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

html, body {
  height: 100%;
  margin: 0;
}

body {
  font: var(--step-0)/1.45 var(--font-ui);
  background: var(--bg);
  color: var(--fg);
  overscroll-behavior: none;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
}

/* Every layout rule below sets `display`, which silently beats the `hidden`
   attribute's user-agent `display: none`. Since JS toggles visibility with
   `.hidden` throughout, this has to win. */
[hidden] { display: none !important; }

h1, h2, h3, p { margin: 0; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
::selection { background: var(--selection); }

/*
 * Mirrored layout, without mangling the English in it.
 *
 * Turning the document right-to-left flips the layout, which is what was
 * wanted — and it also flips the *punctuation* of every English sentence in
 * the interface, because a full stop at the end of a run of Latin text in an
 * RTL paragraph belongs, by the bidirectional algorithm, at the left. So
 * "Nothing here yet." came out as ".Nothing here yet".
 *
 * `plaintext` lets each block take its direction from its own first strong
 * character: Hebrew reads right to left, English reads left to right, and the
 * layout around both stays mirrored. Applied to blocks rather than to inline
 * elements, which would fragment a sentence into separately-directed pieces.
 */
[dir="rtl"] p,
[dir="rtl"] li,
[dir="rtl"] h1, [dir="rtl"] h2, [dir="rtl"] h3, [dir="rtl"] h4,
[dir="rtl"] dt, [dir="rtl"] dd,
[dir="rtl"] th, [dir="rtl"] td,
[dir="rtl"] summary,
[dir="rtl"] label,
[dir="rtl"] button,
[dir="rtl"] option,
[dir="rtl"] .card-title, [dir="rtl"] .card-author, [dir="rtl"] .card-meta {
  unicode-bidi: plaintext;
}

.dim { color: var(--fg-dim); }
.dim em, em.dim { font-style: normal; }

/* ---------------------------------------------------------------- */
/* Screens                                                           */
/* ---------------------------------------------------------------- */

/* ---------------------------------------------------------------- */
/* The flame                                                         */
/* ---------------------------------------------------------------- */

/*
 * The only amber in the product.
 *
 * See the note at the top of tokens.css: `--flame` marks the reading
 * position, the boundary, and the mark. If this colour turns up on an
 * ordinary button or link, the identity is broken — the whole point is that a
 * reader who sees amber knows what it means without being told.
 */
.flame-outer { fill: var(--flame); }
.flame-core { fill: var(--flame-core); }

/* Core only, never the outer shape. An outer edge that moves reads as a logo
   performing a trick; a core that varies reads as a flame, because that is
   what a flame does. */
@media (prefers-reduced-motion: no-preference) {
  .flame-core:not(.flame-still) { animation: flame-flicker var(--flicker) infinite; }
  @keyframes flame-flicker {
    0%   { opacity: 1; }
    18%  { opacity: 0.70; }
    32%  { opacity: 1; }
    51%  { opacity: 0.86; }
    68%  { opacity: 1; }
    83%  { opacity: 0.76; }
    100% { opacity: 1; }
  }
}
[data-epaper] .flame-core { animation: none; }

/* `relative`, so the panels inside measure themselves against the screen
   rather than against the viewport. */
.screen { display: none; height: 100%; flex-direction: column; position: relative; }
body[data-screen="library"] #screen-library,
body[data-screen="reader"]  #screen-reader { display: flex; }

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

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: 0 var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-2);
  color: var(--fg);
  white-space: nowrap;
}
.btn:hover { background: var(--bg-3); }
.btn:active { transform: translateY(1px); }
.btn[disabled] { opacity: 0.45; pointer-events: none; }

.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
.btn-primary:hover { filter: brightness(1.08); background: var(--accent); }
.btn-danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, var(--border)); }
.btn-danger:hover { background: color-mix(in srgb, var(--danger) 12%, var(--bg-2)); }

.btn-icon { width: var(--tap); padding: 0; font-size: var(--step-1); line-height: 1; }

/*
 * A button whose panel is open looks open.
 *
 * Only the read-aloud button had this, and only because it was written by
 * hand. With four panels reachable from one bar and no lit state, the only way
 * to know what was showing was to look at the panel — which is no help at all
 * when the question is "which of these opened it".
 *
 * Deliberately not amber. Amber is the reading position; a lit control is an
 * ordinary piece of interface, and it uses the accent like everything else.
 */
.btn-icon[aria-pressed="true"] {
  background: var(--bg-3);
  border-color: var(--accent);
  color: var(--fg);
}

/*
 * The companion's taper is lit while the companion is open.
 *
 * The exception to the note above, and the narrowest one available: not the
 * button, not its border, not its background — the flame in the icon, being a
 * flame. Everything else about the pressed state stays the ordinary accent, so
 * the companion looks like the other controls and its wick looks alight.
 */
#btn-companion[aria-pressed="true"] .taper-flame {
  fill: var(--flame-core);
  stroke: var(--flame);
}
#btn-companion[aria-pressed="true"] .icon {
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--flame) 65%, transparent));
}
@media (prefers-reduced-motion: no-preference) {
  #btn-companion .taper-flame { transition: fill var(--dur-2, 160ms) ease, stroke var(--dur-2, 160ms) ease; }
}
.btn-wide { width: 100%; }
.btn-sm { min-height: 28px; font-size: var(--step--1); padding: 0 var(--sp-2); }

.input, .select {
  min-height: var(--tap);
  width: 100%;
  padding: 0 var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-2);
  color: var(--fg);
}
.input::placeholder { color: var(--fg-dim); }
.select { padding-inline-end: var(--sp-2); }

input[type="range"] {
  flex: 1;
  min-width: 0;
  accent-color: var(--accent);
  background: transparent;
}

.field { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.field > span { font-size: var(--step--2); text-transform: uppercase; letter-spacing: 0.05em; color: var(--fg-dim); }
.field-row { flex-direction: row; align-items: center; gap: var(--sp-3); }
.field-row > span:first-child { flex: 0 0 5.5rem; text-transform: none; letter-spacing: 0; font-size: var(--step--1); }
.field-row output { flex: 0 0 3.2rem; text-align: end; font-variant-numeric: tabular-nums; font-size: var(--step--1); color: var(--fg-dim); }
/* A dropdown beside its label may shrink: at its natural width — the longest
   choice, a chapter title — it pushed past the edge of a phone-sized panel. */
.field-row .select { min-width: 0; }

.check { display: flex; align-items: flex-start; gap: var(--sp-2); padding: var(--sp-2) 0; cursor: pointer; }
.check input { margin-top: 0.2em; accent-color: var(--accent); width: 16px; height: 16px; flex: none; }
.check span { font-size: var(--step--1); }
.check em { color: var(--fg-dim); font-style: normal; }
.check-warn em { color: var(--danger); }

.segmented { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
.segmented button {
  min-height: var(--tap);
  padding: 0 var(--sp-3);
  border: 0;
  border-inline-start: 1px solid var(--border);
  background: var(--bg-2);
  color: var(--fg-dim);
}
.segmented button:first-child { border-inline-start: 0; }
.segmented button:hover { background: var(--bg-3); }
.segmented button[aria-pressed="true"] { background: var(--accent); color: var(--accent-fg); }
.segmented-wide { display: flex; width: 100%; margin-bottom: var(--sp-2); }
.segmented-wide button { flex: 1; }

/* ---------------------------------------------------------------- */
/* Modal                                                             */
/* ---------------------------------------------------------------- */

.modal {
  border: 0;
  padding: 0;
  background: transparent;
  max-width: min(520px, calc(100vw - 2rem));
  color: var(--fg);
}
.modal::backdrop { background: rgb(0 0 0 / 0.5); }

.modal-panel {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-2);
  box-shadow: var(--shadow);
  padding: var(--sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  max-height: 80vh;
}
.modal-title { font-size: var(--step-1); font-weight: 600; }
.modal-body { overflow: auto; display: flex; flex-direction: column; gap: var(--sp-3); }
.modal-body p { line-height: 1.5; }
/* Running prose in a dialog, set to read rather than to be scanned. */
.prose { display: flex; flex-direction: column; gap: var(--sp-4); }
.prose p { line-height: 1.6; max-width: 62ch; }
/* Why read is an essay, read for its own sake — a size up from the rest of
   the menu's pages (5 Oct). The signature keeps its own, smaller size. */
.why-read p:not(.why-signature) { font-size: 1.1rem; }

/* The name at the end of an argument. Set the way a signature is: smaller than
   the text, and at the right-hand end of it — of the *text*, not of the dialog,
   so it sits under the last line rather than drifting off towards the edge when
   the prose is narrower than the box it is in. */
.why-signature {
  max-width: 62ch;
  width: 100%;
  text-align: right;
  margin-top: calc(var(--sp-2) * -1);
  font-size: var(--step--1);
  font-style: italic;
  color: var(--fg-dim);
}

/* Ask-for-one-thing dialogs: the note, a gap, then the box. */
.prompt-body { display: flex; flex-direction: column; gap: var(--sp-3); }
.prompt-note { font-size: var(--step--1); color: var(--fg-dim); line-height: 1.45; }
/* Back sits apart, on the left: it goes the other way from everything else in
   the row, and a row of buttons that all look alike hides that. */
/* Wrapping, because a dialog with four choices in it is a dialog with four
   choices in it — and without this the first one was cut off by the edge. */
.modal-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--sp-2); }
.modal-actions .btn-back { margin-inline-end: auto; }

/* ---------------------------------------------------------------- */
/* The drawer — the menu and every page opened from it               */
/* ---------------------------------------------------------------- */

/*
 * Pinned to the right-hand edge and the full height of the window, the same
 * shape as the highlights panel, so going from the menu to highlights and back
 * is one panel changing what it shows rather than two different kinds of
 * window. The width is shared with the side panels the menu opens for the same
 * reason: an edge that jumps sideways on every step reads as something going
 * wrong.
 *
 * Still a real <dialog> underneath. Focus stays inside it, Esc closes it, and
 * the page behind cannot be clicked by accident while it is open.
 */
.modal-drawer {
  position: fixed;
  inset-block: 0;
  inset-inline: auto 0;
  margin: 0;
  width: var(--drawer-w);
  max-width: 100vw;
  height: 100%;
  max-height: 100%;
  background: var(--bg-2);
  border: 0;
  border-inline-start: 1px solid var(--border);
  box-shadow: var(--shadow);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
/* Deep enough to read as "the page is behind this" on the dark themes too,
   where three-tenths black over a near-black page barely showed. */
.modal-drawer::backdrop { background: rgb(0 0 0 / 0.38); }

.drawer-panel { display: flex; flex-direction: column; height: 100%; outline: none; }

/* Back on the left, the title beside it, the × at the far end — where every
   side panel in the app keeps its way out. */
/* `.panel-head.drawer-head`: the plain `.panel-head` rule comes later in this
   file and would otherwise win, putting its own spacing back. */
.panel-head.drawer-head { justify-content: flex-start; gap: var(--sp-2); padding-block: 0; padding-inline: var(--sp-4) var(--sp-2); }
.drawer-head .drawer-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.drawer-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--sp-4); }
.drawer-foot { flex: none; padding: var(--sp-3) var(--sp-4); border-top: 1px solid var(--border); }

/*
 * Pages written as plain runs of paragraphs get room between them, and their
 * headings are set as the small section labels every panel uses rather than
 * at the browser's default size, which in a side panel read as a shout.
 */
.drawer-body > div:not([class]) > * + *,
.backup-page > * + * { margin-top: var(--sp-2); }
.drawer-body > div > h3 {
  font-size: var(--step--2);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-dim);
}
.drawer-body > div > h3:not(:first-child) { margin-top: var(--sp-5); }
.backup-warn { color: var(--danger); }

/* Between one page and the next: greyed, and not clickable, for the moment
   before the next page arrives. */
.modal-drawer[data-idle] .drawer-body,
.modal-drawer[data-idle] .drawer-foot { opacity: 0.55; pointer-events: none; }

@media (prefers-reduced-motion: no-preference) {
  .modal-drawer[open] { animation: drawer-in 180ms var(--ease); }
  .modal-drawer[open]::backdrop { animation: drawer-fade 180ms ease; }
  .modal-drawer[data-closing] { animation: drawer-out 160ms var(--ease) forwards; }
  .modal-drawer[data-closing]::backdrop { animation: drawer-fade-out 160ms ease forwards; }
  .modal-drawer .drawer-body { transition: opacity 120ms ease; }
  /* Mirrored, it comes in from the other edge. */
  [dir="rtl"] .modal-drawer[open] { animation-name: drawer-in-rtl; }
  [dir="rtl"] .modal-drawer[data-closing] { animation-name: drawer-out-rtl; }
  @keyframes drawer-in { from { transform: translateX(100%); } }
  @keyframes drawer-out { to { transform: translateX(100%); } }
  @keyframes drawer-in-rtl { from { transform: translateX(-100%); } }
  @keyframes drawer-out-rtl { to { transform: translateX(-100%); } }
  @keyframes drawer-fade { from { opacity: 0; } }
  @keyframes drawer-fade-out { to { opacity: 0; } }
}

/* E-paper: nothing moves, nothing is see-through. The kill switch at the end
   of this file covers elements and their ::before/::after, not ::backdrop,
   and a greyed page between steps ghosts on a partial refresh. */
[data-epaper] .modal-drawer::backdrop { animation: none !important; background: rgb(0 0 0 / 0.5); }
[data-epaper] .modal-drawer[data-idle] .drawer-body,
[data-epaper] .modal-drawer[data-idle] .drawer-foot { opacity: 1; }
[data-epaper] .modal-drawer { border-inline-start-width: 2px; }

/* ---------------------------------------------------------------- */
/* The menu                                                          */
/* ---------------------------------------------------------------- */

/*
 * Short names in four small groups, and nothing else on the line unless it is
 * worth knowing before you press it — how many words are due, that a backup is
 * overdue, that the companion is not set up yet.
 *
 * The previous menu put a sentence under every one of its thirteen entries.
 * Each sentence was reasonable on its own, and together they turned a list you
 * glance at into a page you have to read.
 */
.menu-nav { display: flex; flex-direction: column; gap: var(--sp-4); }
.menu-group { display: flex; flex-direction: column; gap: 1px; }
.menu-group-title {
  font-size: var(--step--2);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-dim);
  padding: 0 var(--sp-3) var(--sp-1);
}
.menu-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  min-height: 44px;
  padding: var(--sp-2) var(--sp-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: inherit;
  text-align: start;
}
.menu-item:hover { background: var(--bg-3); }
.menu-item > .icon { flex: none; color: var(--fg-dim); }
.menu-item-label { flex: 1; min-width: 0; }
.menu-item-note { flex: none; font-size: var(--step--1); color: var(--fg-dim); font-variant-numeric: tabular-nums; }
.menu-item-note[data-tone="warn"] { color: var(--danger); }
.menu-item-go { flex: none; color: var(--fg-dim); opacity: 0.6; }
/* Pointing into the drawer, which is the other way round when mirrored. */
[dir="rtl"] .menu-item-go .icon { transform: scaleX(-1); }

/* ---------------------------------------------------------------- */
/* A search box with its magnifier                                   */
/* ---------------------------------------------------------------- */

/* The icon sits inside the box, at the start, where every search box keeps
   it; the text begins after it and reads from the left like any other field. */
.search-field { position: relative; display: block; min-width: 0; }
.search-field > .icon {
  position: absolute;
  inset-inline-start: var(--sp-2);
  top: 50%;
  transform: translateY(-50%);
  color: var(--fg-dim);
  pointer-events: none;
}
/* `start`, not `left`: mirrored, the magnifier and the text both begin at the
   right, and a Hebrew search reads from where Hebrew starts. */
.search-field > .input { padding-inline-start: calc(var(--sp-2) + 22px); text-align: start; }

/*
 * What a search matched, in yellow.
 *
 * Yellow, not amber, and the difference is kept on purpose: amber (`--flame`)
 * marks the reading position and nothing else. Yellow is the highlighter — the
 * colour a reader already uses in this app to say "this bit" — so a match looks
 * as if the text has been gone over with the same pen. Solid, with dark ink on
 * it, so it reads the same in the dark themes as in the light ones.
 */
mark.hit, .result mark {
  background: #ffe066;
  color: #221c00;
  border-radius: 2px;
  padding: 0 1px;
}
/* On a greyscale e-paper screen pale yellow is nearly white. Underlined and
   bold instead, which survives having no colour at all. */
[data-epaper] mark.hit, [data-epaper] .result mark {
  background: transparent;
  color: inherit;
  font-weight: 700;
  text-decoration: underline 2px;
  text-underline-offset: 2px;
}

/* ---------------------------------------------------------------- */
/* Toasts and busy overlay                                           */
/* ---------------------------------------------------------------- */

.toast-host {
  position: fixed;
  inset: auto 0 calc(var(--sp-5) + env(safe-area-inset-bottom, 0px)) 0;
  z-index: 90;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  pointer-events: none;
}
/* The host is a popover so it can sit in the top layer (see `toast` in ui.js);
   these undo the browser's own popover box — a centred, bordered, painted
   panel — so it stays an invisible strip along the bottom. */
.toast-host[popover] {
  margin: 0;
  width: auto;
  height: auto;
  border: 0;
  padding: 0;
  background: transparent;
  color: inherit;
  overflow: visible;
}
.toast {
  pointer-events: auto;
  max-width: min(460px, calc(100vw - 2rem));
  padding: var(--sp-2) var(--sp-4);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-2);
  color: var(--fg);
  box-shadow: var(--shadow);
  font-size: var(--step--1);
  cursor: pointer;
}
/* ---------------------------------------------------------------- */
/* Overlay panels while a book is open                               */
/* ---------------------------------------------------------------- */

/*
 * The companion, highlights and author panels are `position: fixed` because
 * they also open from the library, where there is no reader to sit inside.
 * While a book *is* open they take the same band as the reader's own panels —
 * below the bar, above the candle — so the page beside them is whole and the
 * turn-page zone is reachable.
 */
body[data-screen="reader"] .companion-panel,
body[data-screen="reader"] .quotes-panel {
  top: var(--bar-actual, var(--bar-h));
  bottom: var(--foot-actual, 0px);
}

/* Full screen lifts the bars out of the layout, so there is no band to sit in
   and the panel takes the whole height again. */
body[data-immersive="true"] .companion-panel,
body[data-immersive="true"] .quotes-panel { top: 0; bottom: 0; }

/* A toast that carries its own button — "an update is ready" is only useful
   with the way to take it attached. */
.toast-with-action { display: inline-flex; align-items: center; gap: var(--sp-3); }

/* ---------------------------------------------------------------- */
/* "You are running an old copy"                                     */
/* ---------------------------------------------------------------- */

/* A bar rather than a toast, because this is the one message that must not be
   missable: everything else on screen may be a version out of date. Above the
   reader's own chrome, and it stays until acted on or closed. */
.stale-bar {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  padding: var(--sp-2) var(--sp-4);
  background: var(--bg-2);
  border-bottom: 1px solid var(--border);
  color: var(--fg);
  font-size: var(--step--1);
  box-shadow: var(--shadow);
}
/* Red for "you are looking at the wrong code" — amber for "the server is not
   answering", which is a thing to go and fix rather than a thing that is wrong. */
.stale-bar-old {
  background: color-mix(in srgb, var(--danger) 16%, var(--bg-2));
  border-bottom-color: color-mix(in srgb, var(--danger) 55%, var(--border));
}
/* `--warn` is defined by no theme, so this fell through to the accent and the
   "server not answering" bar came out grey. An amber of its own as the
   fallback, which is what this bar was always meant to be. */
.stale-bar-offline {
  background: color-mix(in srgb, var(--warn, #d08a1e) 16%, var(--bg-2));
  border-bottom-color: color-mix(in srgb, var(--warn, #d08a1e) 55%, var(--border));
}
.stale-bar-close { padding-inline: var(--sp-2); }

/* Move the screens down by the bar's height rather than letting it sit over
   their headers. Applied to the screens rather than to `body`, because the
   reader's layout is measured against the viewport and padding on `body` would
   push its last line off the bottom. */
[data-stale] .screen { padding-top: var(--stale-h, 46px); box-sizing: border-box; }

/*
 * And the panels below it too.
 *
 * The screens moved down but the panels did not: they are placed from the top
 * of the window, so the bar sat over their headers — the title, the ×, and
 * "‹ Menu" — at exactly the moment the reader was being told to reload.
 */
[data-stale] body[data-screen="library"] .quotes-panel,
[data-stale] body[data-screen="library"] .companion-panel,
[data-stale] body[data-immersive="true"] .panel,
[data-stale] body[data-immersive="true"] .quotes-panel,
[data-stale] body[data-immersive="true"] .companion-panel { top: var(--stale-h, 46px); }
[data-stale] body:not([data-immersive="true"]) .panel,
[data-stale] body[data-screen="reader"]:not([data-immersive="true"]) .quotes-panel,
[data-stale] body[data-screen="reader"]:not([data-immersive="true"]) .companion-panel {
  top: calc(var(--stale-h, 46px) + var(--bar-actual, var(--bar-h)));
}

/* "×3" on a message that has happened three times. See `toast` in ui.js. */
.toast-count { font-variant-numeric: tabular-nums; opacity: 0.75; font-weight: 600; }

.toast-success { border-color: color-mix(in srgb, var(--ok) 55%, var(--border)); }
.toast-error { border-color: color-mix(in srgb, var(--danger) 55%, var(--border)); color: var(--danger); }

.busy {
  position: fixed;
  inset: 0;
  z-index: 95;
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--bg) 78%, transparent);
}
.busy-panel {
  width: min(360px, calc(100vw - 3rem));
  padding: var(--sp-5);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-2);
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}
.busy-label { font-size: var(--step--1); overflow-wrap: anywhere; }
.busy-note { font-size: var(--step--2); min-height: 1.2em; margin: 0; }
.busy-bar { height: 4px; border-radius: 2px; background: var(--bg-3); overflow: hidden; }
.busy-bar-fill { height: 100%; width: 30%; background: var(--accent); }

/*
 * Work with no progress to report still has to look like work.
 *
 * This used to be a static dim bar, which is indistinguishable from a frozen
 * app — and on a slow provider that is exactly how it was read. It now sweeps.
 */
.busy:not([data-determinate]) .busy-bar-fill {
  width: 35%;
  animation: busy-sweep 1.3s ease-in-out infinite;
}

@keyframes busy-sweep {
  0%   { margin-left: -35%; }
  100% { margin-left: 100%; }
}

/* E-paper cannot animate, and reduced-motion means it should not. Both get a
   full dim bar instead — static, but the seconds counter still moves. */
[data-epaper] .busy:not([data-determinate]) .busy-bar-fill { animation: none; width: 100%; opacity: 0.35; }

@media (prefers-reduced-motion: reduce) {
  .busy:not([data-determinate]) .busy-bar-fill { animation: none; width: 100%; opacity: 0.35; }
}

/* ---------------------------------------------------------------- */
/* Slide-over panels (reader)                                        */
/* ---------------------------------------------------------------- */

/*
 * A panel spans the gap between the bar and the footer, and not one pixel more.
 *
 * It used to run the full height of the screen, which put its last few
 * centimetres underneath the footer — invisible, and unreachable, because the
 * scrolling area extended down there too. It had always been slightly wrong
 * and became obvious when the candle made the footer taller: the bottom of the
 * language panel simply could not be scrolled to.
 *
 * The two heights are measured by reader.js and written here, rather than
 * being constants that have to be kept in step with the type scale, the
 * reading settings and the footer's contents by hand.
 */
.panel {
  position: absolute;
  top: var(--bar-actual, var(--bar-h));
  bottom: var(--foot-actual, 0px);
  z-index: 30;
  width: var(--panel-w);
  max-width: 88vw;
  display: flex;
  flex-direction: column;
  background: var(--bg-2);
  border: 0 solid var(--border);
}
.panel-wide { width: var(--panel-w-wide); }
/* Start and end rather than left and right, so the right-to-left mirror puts
   each panel on the same side as the button that opens it. With left/right,
   the mirrored bar had Contents on the right opening a panel on the left. */
.panel-left  { inset-inline-start: 0; border-inline-end-width: 1px; }
.panel-right { inset-inline-end: 0;   border-inline-start-width: 1px; }
.panel[hidden] { display: none; }

.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  min-height: var(--bar-h);
  padding: 0 var(--sp-2) 0 var(--sp-4);
  border-bottom: 1px solid var(--border);
  flex: none;
}
.panel-head h2 { font-size: var(--step--1); text-transform: uppercase; letter-spacing: 0.07em; color: var(--fg-dim); font-weight: 600; }
/* On a narrow screen the title gives way, never the buttons: a back button, a
   title and two actions do not all fit in 320 pixels, and a cut-off title still
   says where you are while a cut-off button cannot be pressed. */
.panel-head h2 { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.panel-head > .rd-bar-actions, .panel-head > .panel-back, .panel-head > .panel-close { flex: none; }
.panel-body { flex: 1; overflow-y: auto; padding: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-3); }

.result-list { display: flex; flex-direction: column; gap: 1px; }
.result {
  display: block;
  width: 100%;
  text-align: start;
  padding: var(--sp-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: inherit;
}
.result:hover { background: var(--bg-3); }
.result-label { display: flex; justify-content: space-between; gap: var(--sp-2); font-size: var(--step--1); }
.result-snippet { color: var(--fg-dim); font-size: var(--step--1); margin-top: 2px; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
/* Search matches in a result list are marked like every other search match —
   see `mark.hit` above. */
.result-meta { font-size: var(--step--2); color: var(--fg-dim); font-variant-numeric: tabular-nums; }

.empty-note { color: var(--fg-dim); font-size: var(--step--1); padding: var(--sp-4) 0; text-align: center; }

/* ---------------------------------------------------------------- */
/* Touch screens                                                     */
/* ---------------------------------------------------------------- */

/*
 * Controls that wait for the pointer to hover are invisible on a screen with
 * no pointer to hover.
 *
 * Four of them did exactly that — a book's ⋯ details, a collection's ⋯, the ×
 * that deletes a bookmark or highlight, and Recap beside a finished chapter —
 * so on a phone or a tablet they could not be found, and there was no other
 * way to delete a bookmark at all. Where there is no hover they are simply
 * shown. `:root` so this wins over each file's own "hidden until hover" rule,
 * which the later stylesheets would otherwise win by order alone.
 */
@media (hover: none) {
  :root .card-menu,
  :root .rail-item .edit,
  :root .result-del,
  :root .toc-summarise { opacity: 1; }
}

/* ---------------------------------------------------------------- */
/* Motion — the only place it is defined                             */
/* ---------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  .btn, .segmented button, .result, .toast { transition: background var(--speed) var(--ease); }
  .toast { animation: toast-in var(--speed) var(--ease); }
  @keyframes toast-in { from { opacity: 0; transform: translateY(6px); } }
}

/* ---------------------------------------------------------------- */
/* E-paper mode                                                      */
/* ---------------------------------------------------------------- */

[data-epaper] {
  --radius: 0px;
  --radius-sm: 0px;
  --tap: 46px;
  --shadow: none;
}
[data-epaper] *,
[data-epaper] *::before,
[data-epaper] *::after {
  transition: none !important;
  animation: none !important;
  scroll-behavior: auto !important;
  box-shadow: none !important;
  text-shadow: none !important;
}
/* Partial refresh leaves ghosting behind translucent layers, so everything
   that would normally sit on a tint becomes fully opaque with a hard rule. */
[data-epaper] .toast,
[data-epaper] .modal-panel,
[data-epaper] .busy-panel { border-width: 2px; }
[data-epaper] .busy { background: var(--bg); }
[data-epaper] .btn:active { transform: none; }

/* ---------------------------------------------------------------- */
/* Small screens                                                     */
/* ---------------------------------------------------------------- */

@media (max-width: 720px) {
  :root { --tap: 40px; --panel-w: 100vw; --panel-w-wide: 100vw; }
  /* E-paper's larger targets win here too — this rule came later and quietly
     shrank them back on exactly the small e-ink readers that need them. */
  :root[data-epaper] { --tap: 46px; }
  .panel { max-width: 100vw; }
}

/* ------------------------------------------------------------------ */
/* Reading stats and the spending ledger (Phase 6)                     */
/* ------------------------------------------------------------------ */

.stat-big {
  font-size: 1.6rem;
  line-height: 1.2;
  margin: 0.2em 0 0.1em;
  font-weight: 600;
}

/* The stats page only. It also caught `.set-group + .set-group h3`, so in the
   reading and Marks panels every section heading after the first was a
   different size, fainter, and further down than the first one. */
.stats-body h3 {
  margin: 1.4em 0 0.4em;
  font-size: 0.82rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.6;
}

.stat-table {
  width: 100%;
  border-collapse: collapse;
  margin: 0.3em 0 0.6em;
}

.stat-table th,
.stat-table td {
  text-align: start;
  padding: 0.32em 0;
  border-bottom: 1px solid var(--rule, rgba(128, 128, 128, 0.22));
  font-weight: 400;
}

.stat-table td {
  text-align: end;
  font-variant-numeric: tabular-nums;
}

.stat-table tr:last-child th,
.stat-table tr:last-child td { border-bottom: 0; }

/* The read-aloud button, which is a toggle and should look like one. */
#btn-speak.is-on {
  background: color-mix(in srgb, currentColor 14%, transparent);
}

/* ------------------------------------------------------------------ */
/* First run                                                           */
/* ------------------------------------------------------------------ */

.welcome h3 {
  margin: 1.3em 0 0.3em;
  font-size: 0.82rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.6;
}

.welcome-lead {
  font-size: 1.05rem;
  line-height: 1.5;
}

.welcome ul { margin: 0.3em 0; padding-inline-start: 1.1em; }
.welcome li { margin-bottom: 0.5em; }
