/*
 * Quran Player — the whole stylesheet.
 *
 * The palettes are lifted from the Android project's `ui/theme/Color.kt` and
 * `ui/theme/ReaderPalette.kt`, including the reasoning recorded there: the light
 * theme is warm parchment rather than white because a sheet of #FFFFFF behind
 * Qur'anic text glares at any size and the app is meant to be left open; the
 * AMOLED theme is true black because those pixels switch off; the panels in the
 * light theme are a shade *darker* than the page, because a panel over paper
 * reads as a panel by being deeper than the sheet, not brighter than everything.
 *
 * Structure: tokens, reset, layout, then one section per surface.
 */

/* ---------------------------------------------------------------------------
   Fonts. Only the four critical faces are declared with `preload` in the HTML;
   the rest are declared here and fetched only if a setting selects them.
   --------------------------------------------------------------------------- */

@font-face {
  font-family: 'UthmanicHafs';
  src: url('./fonts/UthmanicHafs.woff2') format('woff2');
  font-display: block; /* see .caption-line — a swap here would resize the ayah */
}
@font-face { font-family: 'AmiriQuran'; src: url('./fonts/AmiriQuran.woff2') format('woff2'); font-display: block; }
@font-face { font-family: 'me_quran'; src: url('./fonts/me_quran.woff2') format('woff2'); font-display: block; }
@font-face { font-family: 'ScheherazadeNew'; src: url('./fonts/ScheherazadeNew.woff2') format('woff2'); font-display: block; }
@font-face { font-family: 'IndopakNastaleeq'; src: url('./fonts/IndopakNastaleeq.woff2') format('woff2'); font-display: block; }
@font-face { font-family: 'KFGQPCNastaleeq'; src: url('./fonts/KFGQPCNastaleeq.woff2') format('woff2'); font-display: block; }

@font-face { font-family: 'ArefRuqaa'; src: url('./fonts/ArefRuqaa-Bold.woff2') format('woff2'); font-weight: 700; font-display: swap; }
@font-face { font-family: 'ReemKufi'; src: url('./fonts/ReemKufi-Bold.woff2') format('woff2'); font-weight: 700; font-display: swap; }
@font-face { font-family: 'Amiri'; src: url('./fonts/Amiri-Bold.woff2') format('woff2'); font-weight: 700; font-display: swap; }
@font-face { font-family: 'Lateef'; src: url('./fonts/Lateef.woff2') format('woff2'); font-display: swap; }
@font-face { font-family: 'Cairo'; src: url('./fonts/Cairo-SemiBold.woff2') format('woff2'); font-weight: 600; font-display: swap; }

@font-face { font-family: 'Cinzel'; src: url('./fonts/Cinzel-SemiBold.woff2') format('woff2'); font-weight: 600; font-display: swap; }
@font-face { font-family: 'CormorantGaramond'; src: url('./fonts/CormorantGaramond-SemiBold.woff2') format('woff2'); font-weight: 600; font-display: swap; }
@font-face { font-family: 'Marcellus'; src: url('./fonts/Marcellus.woff2') format('woff2'); font-display: swap; }
@font-face { font-family: 'Spectral'; src: url('./fonts/Spectral-SemiBold.woff2') format('woff2'); font-weight: 600; font-display: swap; }

/* The calligraphic plaque. `liga` must be on: the font substitutes one glyph for
   the literal string "surah018". */
@font-face { font-family: 'SurahNames'; src: url('./fonts/surah_names.woff2') format('woff2'); font-display: swap; }
@font-face { font-family: 'SurahNamesV4'; src: url('./fonts/surah_names_v4.woff2') format('woff2'); font-display: swap; }
@font-face { font-family: 'SurahHeader'; src: url('./fonts/surah_header.woff2') format('woff2'); font-display: swap; }

/* ---------------------------------------------------------------------------
   Tokens
   --------------------------------------------------------------------------- */

:root {
  /* The per-reciter gold, set from the catalogue at runtime. */
  --accent: #c9a227;
  --gold: #d4af37;
  --gold-soft: #e8c55a;
  --gold-deep: #8a6f1e;
  --emerald: #2e7d63;

  --ui: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --arabic-ui: 'Cairo', 'Segoe UI', Tahoma, sans-serif;
  --display-ar: 'ArefRuqaa', serif;
  --display-la: 'Cinzel', Georgia, serif;
  --quran: 'UthmanicHafs', 'AmiriQuran', serif;
  --plaque: 'SurahNames', 'ArefRuqaa', serif;

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

  --transport-h: 92px;
  --pad: clamp(16px, 4vw, 40px);

  color-scheme: dark;
}

/* Night — the default. */
[data-theme='night'] {
  --page: #0b0b0e;
  --page-edge: #000;
  --surface: #15151a;
  --surface-high: #1e1e25;
  --ink: #e9e9ef;
  --ink-dim: rgb(233 233 239 / 0.72);
  --ink-faint: rgb(233 233 239 / 0.45);
  --panel: rgb(0 0 0 / 0.66);
  --panel-strong: rgb(0 0 0 / 0.82);
  --panel-border: rgb(255 255 255 / 0.16);
  --outline: #3a3a45;
  --scene-tint: #8496b4;
  --scene-alpha: 0.62;
  --vignette: rgb(0 0 0 / 0.52);
  --mote: #fff;
  --shadow: 0 2px 18px rgb(0 0 0 / 0.55);
  color-scheme: dark;
}

/* Pure black. On an OLED panel these pixels are switched off — the deepest
   contrast available for the caption and the lowest draw for a screen that may
   be left on through a whole juz. */
[data-theme='amoled'] {
  --page: #000;
  --page-edge: #000;
  --surface: #000;
  --surface-high: #101014;
  /* Soft off-white, not #FFFFFF. Pure white on true black is the highest-contrast
     pairing a screen can make and it glares badly in the dark, which is exactly
     when this theme gets used. */
  --ink: #e8e8ee;
  --ink-dim: rgb(232 232 238 / 0.72);
  --ink-faint: rgb(232 232 238 / 0.45);
  --panel: rgb(0 0 0 / 0.72);
  --panel-strong: rgb(0 0 0 / 0.9);
  --panel-border: rgb(255 255 255 / 0.14);
  --outline: #34343e;
  --scene-tint: #8496b4;
  --scene-alpha: 0.5;
  --vignette: rgb(0 0 0 / 0.6);
  --mote: #fff;
  --shadow: 0 2px 18px rgb(0 0 0 / 0.7);
  color-scheme: dark;
}

/* Parchment. */
[data-theme='parchment'] {
  --page: #fbf7ee;
  --page-edge: #ede4d2;
  --surface: #fffbf2;
  --surface-high: #f5efe0;
  --ink: #241f17;
  --ink-dim: rgb(36 31 23 / 0.68);
  --ink-faint: rgb(36 31 23 / 0.42);
  /* Warm, and a shade darker than the page — not white. A panel over paper reads
     as a panel by being slightly deeper than the sheet, the way a card resting on
     it would be. */
  --panel: rgb(242 234 215 / 0.92);
  --panel-strong: rgb(247 241 227 / 0.97);
  --panel-border: rgb(36 31 23 / 0.16);
  --outline: #bfb69f;
  /* Darker than the page, so the building reads as a silhouette on it rather than
     washing out. */
  --scene-tint: #6b6353;
  --scene-alpha: 0.3;
  --vignette: rgb(107 90 56 / 0.13);
  --mote: #8a7a4a;
  --shadow: 0 2px 14px rgb(90 75 40 / 0.18);
  --accent-ink: var(--gold-deep);
  color-scheme: light;
}

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

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

/*
 * `hidden` must actually hide.
 *
 * The UA rule is `[hidden] { display: none }`, which any later `display`
 * declaration overrides — and several components here set `display: grid` on the
 * same elements they toggle with `.hidden`. That silently produced a caption with
 * the pre-roll ornament drawn on top of it. Raising the specificity once is far
 * safer than remembering the interaction at every call site.
 */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: var(--page);
  color: var(--ink);
  font-family: var(--ui);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overscroll-behavior: none;
}

[dir='rtl'] body { font-family: var(--arabic-ui); }

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

button {
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

::selection { background: color-mix(in srgb, var(--accent) 35%, transparent); }

/* Scrollbars, so a list does not put a bright system bar over a dark page. */
* { scrollbar-width: thin; scrollbar-color: var(--outline) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-thumb { background: var(--outline); border-radius: 99px; border: 3px solid transparent; background-clip: content-box; }
*::-webkit-scrollbar-track { background: transparent; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
[data-reduced-motion='true'] *, [data-reduced-motion='true'] *::before, [data-reduced-motion='true'] *::after {
  animation-duration: 0.01ms !important;
  transition-duration: 0.01ms !important;
}

/* ---------------------------------------------------------------------------
   App shell
   --------------------------------------------------------------------------- */

#app {
  position: fixed;
  inset: 0;
  display: grid;
  grid-template-rows: 1fr auto;
  /*
   * The column must be pinned to the viewport, and its children must be allowed
   * to shrink inside it.
   *
   * A grid's implicit column is `auto`, which sizes to its widest item's
   * max-content — and a grid item's default `min-width: auto` refuses to shrink
   * below its own content. Together those two defaults let one long Arabic line
   * push this 390 px-wide element's *column* to 451 px, which pushed `.view` to
   * 451 px, which `overflow: hidden` then clipped. The caption measures its
   * container to decide how wide to wrap, so the inflated width fed straight back
   * into a wider line and the layout never settled.
   */
  grid-template-columns: minmax(0, 1fr);
  isolation: isolate;
}

#app > * { min-width: 0; }

/*
 * The view is a two-row grid: the plaque takes what it needs, the caption takes
 * the rest.
 *
 * It was previously a block box with the caption at `height: 100%`, which meant
 * the caption asked for the full view height *in addition to* the plaque above
 * it — so the last line of every long ayah was clipped by the transport. The
 * caption's fitting logic measures its own box, so an honest box is what makes
 * the fit correct rather than merely plausible.
 *
 * The scene, the top bar and the banner are all absolutely positioned and so
 * take part in neither row.
 */
.view {
  position: relative;
  overflow: hidden;
  min-height: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  /*
   * The column is `minmax(0, 1fr)`, not the implicit `auto`.
   *
   * A single-column grid with an auto column sizes the track to its widest item's
   * max-content — so a long Arabic line made the track wider than the viewport,
   * `overflow: hidden` clipped the excess, and the caption ran off the side of a
   * phone. Worse, the caption measures its own container to decide how wide to
   * wrap, so the inflated track fed straight back into a wider line: the layout
   * had no reason to ever settle. `minmax(0, 1fr)` pins the track to the
   * container and lets the contents wrap inside it.
   */
  grid-template-columns: minmax(0, 1fr);
}

/* ---------------------------------------------------------------------------
   The scene: a night mosque under a vignette, with slow motes.
   --------------------------------------------------------------------------- */

.scene {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  background: radial-gradient(120% 90% at 50% 15%, var(--page) 0%, var(--page-edge) 100%);
}

/*
 * The mosque.
 *
 * The frame is held to the *picture's own* proportions — 606 × 898, a portrait —
 * and never to the window's. That is the whole trick, and the Android version
 * says why: with the frame coinciding with the image, the fade below runs along
 * the picture's real edges instead of somewhere out in the empty space beside it,
 * and nothing is stretched.
 *
 * Getting this wrong is what made the scene look broken: the frame was forced to
 * `width: 100%; height: 56vh` — a wide, short box — and `object-fit: cover`
 * anchored to the bottom then cropped a tall portrait down to a strip of its
 * lowest few hundred pixels. The mosque was never on screen at all. What showed
 * was the edge of a sand dune, running diagonally off the right of the page,
 * which is exactly what it looked like: a stray dark triangle.
 */
.scene__frame {
  position: absolute;
  left: 50%;
  /* Bled off the bottom edge, so the picture never ends on a line the transport
     does not already cover. */
  bottom: -6%;
  translate: -50% 0;
  height: min(96vh, 900px);
  aspect-ratio: 606 / 898;
  max-width: 130%;
  opacity: var(--scene-alpha);
  /*
   * An oval dissolve, not two straight ramps. A ramp along each axis still leaves
   * the picture ending on four straight lines — softly, but the corners give the
   * rectangle away. A radial mask has no edge anywhere.
   *
   * The gradient starts falling immediately rather than holding a solid core. A
   * solid centre sat right on the picture's near-white sky and printed a bright
   * egg on the page — the mask became the most visible thing in the scene. Fading
   * from the middle outwards keeps the silhouette, which is the part worth
   * seeing, and lets the sky dissolve.
   */
  /*
   * The radii are 50% and 44% of the box, and they have to be — a radial-gradient's
   * percentage radii are measured against the box, so anything above 50%
   * horizontally puts the transparent stop *outside* the frame. The gradient then
   * never reaches zero before the edge, and the picture ends on a straight line
   * after all: with 68% here, the scene showed a plainly visible rectangle with
   * hard left and right sides. Fading to nothing inside the box is the entire
   * point of masking it.
   */
  -webkit-mask-image:
    radial-gradient(ellipse 50% 44% at 50% 58%,
      #000 0%, rgba(0, 0, 0, 0.9) 26%, rgba(0, 0, 0, 0.6) 48%,
      rgba(0, 0, 0, 0.28) 68%, rgba(0, 0, 0, 0.08) 86%, transparent 100%);
  mask-image:
    radial-gradient(ellipse 50% 44% at 50% 58%,
      #000 0%, rgba(0, 0, 0, 0.9) 26%, rgba(0, 0, 0, 0.6) 48%,
      rgba(0, 0, 0, 0.28) 68%, rgba(0, 0, 0, 0.08) 86%, transparent 100%);
  /* Contains the tint blend below, so it multiplies with the photograph and not
     with the page behind it. */
  isolation: isolate;
}

.scene__art {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* Deepens the sky and holds the silhouette, so the picture sits behind the
     text rather than competing with it. */
  filter: brightness(0.72) contrast(1.12);
}

/*
 * One colour over the photograph, multiplied.
 *
 * The picture is high-key: a near-white sky (225) behind a dark mosque (35) and
 * black dunes. Laid on a night page untinted, that sky is a bright slab. Multiply
 * by the scene tint and the sky becomes the mid blue-grey of a moonlit night
 * while the silhouette stays black — which is what makes it read as a scene seen
 * through the dark rather than a photograph pasted onto the page.
 */
.scene__frame::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--scene-tint);
  mix-blend-mode: multiply;
  pointer-events: none;
}

.scene__vignette {
  position: absolute;
  inset: 0;
  background: radial-gradient(100% 75% at 50% 42%, transparent 45%, var(--vignette) 100%);
}

.scene__motes { position: absolute; inset: 0; opacity: 0.5; }

/* ---------------------------------------------------------------------------
   The plaque: calligraphic surah name over a letter-spaced transliteration.
   --------------------------------------------------------------------------- */

.plaque {
  display: grid;
  justify-items: center;
  gap: 6px;
  /* Top padding clears the absolutely-positioned top bar, which would otherwise
     sit over the plaque on a short viewport. */
  padding-block: clamp(46px, 7vh, 74px) clamp(10px, 2.4vh, 26px);
  z-index: 2;
  position: relative;
}

.plaque__glyph {
  font-family: var(--plaque);
  /* The ligature is the whole mechanism: the font substitutes one calligraphic
     glyph for the literal string "surah018". Without `liga` on, eight Latin
     letters appear instead. */
  font-feature-settings: 'liga' 1, 'dlig' 1, 'calt' 1;
  font-variant-ligatures: common-ligatures discretionary-ligatures;
  font-size: clamp(38px, 7.5vh, 72px);
  line-height: 1.15;
  color: var(--accent);
  text-shadow: 0 1px 10px color-mix(in srgb, var(--accent) 30%, transparent);
}

.plaque__glyph--text {
  font-family: var(--display-ar);
  font-size: clamp(28px, 5vh, 48px);
}

.plaque__latin {
  font-family: var(--display-la);
  font-weight: 600;
  font-size: clamp(11px, 1.5vh, 15px);
  letter-spacing: 0.34em;
  text-indent: 0.34em; /* recentre: letter-spacing adds a trailing gap */
  text-transform: uppercase;
  color: var(--ink-dim);
}

.plaque__meta {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 12px;
  color: var(--ink-faint);
  letter-spacing: 0.04em;
}

/* The mushaf ornament rule: a hairline with a small rhombus at its centre. */
.rule {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: min(420px, 68%);
  color: var(--accent);
  opacity: 0.6;
}
.rule::before, .rule::after {
  content: '';
  flex: 1;
  height: 1px;
  background: currentColor;
}
.rule__lozenge {
  width: 7px;
  height: 7px;
  background: currentColor;
  transform: rotate(45deg);
  flex: none;
}

/* ---------------------------------------------------------------------------
   The caption
   --------------------------------------------------------------------------- */

.caption {
  position: relative;
  z-index: 2;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: clamp(10px, 2vh, 22px);
  padding-inline: var(--pad);
  /*
   * The container's height is fixed by the grid, not by its contents.
   *
   * CLAUDE.md's fifth landmine: auto-fitted text resizes when anything around it
   * changes size, so if the ayah is fitted to a box that grows when a control
   * appears, the text visibly jumps. Reserving the space unconditionally means
   * the measurement target never moves.
   */
  /* Both zeroed: a grid item's default `min-*: auto` refuses to shrink below its
     content, which is how an over-wide line keeps its container over-wide. */
  min-height: 0;
  min-width: 0;
  height: 100%;
  text-align: center;

  /*
   * Scrollable, because some ayat genuinely do not fit.
   *
   * The fitting logic shrinks the text to the smallest readable size and then
   * stops — and Al-Baqarah 282, at 144 words the longest ayah in the Quran, still
   * needs about three screens at that size. Clipping it would hide most of the
   * verse; shrinking further would make it unreadable, which is worse. A printed
   * mushaf solves this by turning the page, and this is the same answer.
   *
   * Almost every ayah fits and never scrolls. When one does not, the caption
   * follows the recitation (see mountCaption), so the reader is carried through
   * it rather than having to chase it.
   */
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
}
.caption::-webkit-scrollbar { width: 0; height: 0; }

/* The body is centred while it fits and top-aligned once it overflows, so a long
   ayah starts at its beginning rather than in the middle. */
.caption__body {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  gap: inherit;
  margin: auto 0;
  min-width: 0;
}
.caption[data-overflowing='true'] { align-content: start; }
.caption[data-overflowing='true'] .caption__body { margin: 0; }

.caption-line {
  font-family: var(--quran);
  direction: rtl;
  line-height: 1.95;
  color: var(--ink);
  /*
   * Set in pixels from JavaScript, and it must be, because the fitting logic
   * measures at exactly this width.
   *
   * It used to be `22ch`, which is a *font-relative* unit — so the real element
   * wrapped at 22 characters while the offscreen ruler measured at the container
   * width, and the two disagreed by a factor of three. The fit therefore reported
   * that a 16-word ayah with a translation fitted comfortably at 64px when it
   * actually needed 721px in a 527px box. Measuring one layout and rendering
   * another is the whole class of bug here.
   */
  max-width: var(--caption-w, 100%);
  /* `text-wrap: balance` on a long Arabic run gives a much more mushaf-like
     block than ragged filling, and it is free where supported. */
  text-wrap: balance;
}

[data-theme='night'] .caption-line,
[data-theme='amoled'] .caption-line { text-shadow: 0 2px 12px rgb(0 0 0 / 0.6); }

.caption-line--neighbour {
  color: var(--ink-faint);
  font-size: 0.62em;
  line-height: 1.8;
}

.caption-word {
  /* Transitioning colour rather than toggling it stops the highlight strobing at
     the pace of a fast reciter. */
  transition: color 140ms ease, text-shadow 140ms ease;
}

.caption-word--active {
  color: var(--accent);
  text-shadow: 0 0 18px color-mix(in srgb, var(--accent) 45%, transparent);
}

.caption-ayah-mark {
  color: var(--accent);
  opacity: 0.85;
  font-size: 0.8em;
  padding-inline: 0.15em;
}

/* The Latin transliteration, between the Arabic and the translation. Italic and
   letter-spaced so it reads as a pronunciation guide rather than as prose. */
.caption-transliteration {
  font-family: var(--display-la);
  font-size: clamp(12px, 1.7vh, 15px);
  line-height: 1.7;
  letter-spacing: 0.06em;
  color: var(--accent);
  opacity: 0.82;
  max-width: var(--prose-w, 100%);
  text-align: center;
}

.caption-translation {
  font-family: var(--ui);
  font-size: clamp(13px, 1.9vh, 17px);
  line-height: 1.6;
  color: var(--ink-dim);
  max-width: var(--prose-w, 100%);
  text-align: center;
}
/*
 * No `direction` here on purpose. It is set on the element from the selected
 * translation's own language — see the note in src/ui/caption.js. The rule that
 * used to live here keyed off the *interface* direction, which is right only when
 * the reader's interface and their translation happen to run the same way.
 */

/* The pre-roll ornament: what shows when the timeline says nothing about this
   moment. Deliberately not a caption — see captionSync.js. */
.caption-preroll {
  display: grid;
  justify-items: center;
  gap: 18px;
  color: var(--accent);
  opacity: 0.5;
}
.caption-preroll__mark { font-size: clamp(40px, 8vh, 76px); font-family: var(--quran); }

.caption-static-bismillah {
  font-family: var(--quran);
  direction: rtl;
  font-size: clamp(20px, 3.4vh, 34px);
  color: var(--accent);
  opacity: 0.9;
  margin-block-end: 6px;
}

/* ---------------------------------------------------------------------------
   The transport.
   Pinned to a physical corner, so it does NOT mirror — CLAUDE.md's third
   landmine. The element carries dir="ltr" and the order is fixed in every
   language, because a play/pause/next cluster reads left to right everywhere.
   --------------------------------------------------------------------------- */

.transport {
  position: relative;
  z-index: 5;
  display: grid;
  gap: 10px;
  padding: 12px var(--pad) max(12px, env(safe-area-inset-bottom));
  background: linear-gradient(to bottom, transparent, var(--panel) 22%);
  backdrop-filter: blur(18px) saturate(1.1);
  -webkit-backdrop-filter: blur(18px) saturate(1.1);
  border-top: 1px solid var(--panel-border);
}

.transport__row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(8px, 2vw, 20px);
}

.transport__spacer { flex: 1; }

.iconbtn {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  color: var(--ink-dim);
  transition: color 140ms ease, background 140ms ease, transform 120ms ease;
  flex: none;
}
.iconbtn:hover { color: var(--ink); background: color-mix(in srgb, var(--ink) 8%, transparent); }
.iconbtn:active { transform: scale(0.92); }
.iconbtn[aria-disabled='true'] { opacity: 0.3; pointer-events: none; }
.iconbtn svg { width: 22px; height: 22px; fill: currentColor; }

/* ---------------------------------------------------------------------------
   Labelled buttons
   --------------------------------------------------------------------------- */

/*
 * A transport button with its name under it. The glyph keeps its 44 px hit area;
 * the label sits below without enlarging it, so the row's rhythm is unchanged.
 */
.iconbtn--labelled {
  width: auto;
  min-width: 68px;
  height: auto;
  padding: 4px 8px 3px;
  border-radius: var(--r-sm);
  gap: 3px;
  grid-auto-flow: row;
}
.iconbtn__glyph { display: grid; place-items: center; height: 26px; }
.iconbtn__label {
  font-size: 10.5px;
  letter-spacing: 0.02em;
  line-height: 1.2;
  color: var(--ink-faint);
  white-space: nowrap;
}
.iconbtn--labelled:hover .iconbtn__label { color: var(--ink-dim); }

/* Navigation buttons in the top bar: glyph and word side by side. */
.navbtn {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 7px 11px;
  border-radius: 99px;
  color: var(--ink-dim);
  border: 1px solid transparent;
  transition: color 140ms ease, background 140ms ease, border-color 140ms ease;
  flex: none;
  max-width: 100%;
}
.navbtn:hover {
  color: var(--ink);
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  border-color: var(--panel-border);
}
.navbtn[aria-disabled='true'] { opacity: 0.35; pointer-events: none; }
.navbtn__glyph { display: grid; place-items: center; width: 20px; height: 20px; flex: none; }
.navbtn__glyph svg { width: 19px; height: 19px; fill: currentColor; }
.navbtn__label {
  font-size: 12.5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/*
 * Below 720 px six labels will not fit beside the reciter's name, so the words
 * are dropped and the buttons fall back to glyphs. The `aria-label` and the
 * tooltip still carry the name, so nothing is lost to a screen reader.
 */
@media (max-width: 720px) {
  .navbtn__label { display: none; }
  .navbtn { padding: 9px; }
  /*
   * The split control keeps both halves on a phone — the picker half is the only
   * way into 126 translations, and dropping it would leave a toggle with no way
   * to say what it toggles. The padding comes right down instead, and the
   * chevron shrinks, so the pair costs about as much width as one ordinary
   * button did.
   */
  .navbtn--splitmain { padding-block: 9px; padding-inline: 8px 4px; }
  .navbtn--splitend { padding-block: 9px; padding-inline: 3px 6px; }
  .navbtn__glyph--chev, .navbtn__glyph--chev svg { width: 12px; height: 12px; }
  .iconbtn__label { display: none; }
  .iconbtn--labelled { min-width: 44px; padding: 0; height: 44px; border-radius: 50%; }
}

/*
 * The transport, made to fit a phone.
 *
 * Seven controls and two timecodes at desktop sizes come to about 416 px, which
 * does not fit a 390 px screen: the row overflowed and the elapsed/total times
 * were clipped off the right edge. The flexible spacers go, the buttons come down
 * a few pixels, and the row spreads itself across the width instead.
 */
@media (max-width: 720px) {
  .transport { padding-inline: 12px; }
  .transport__spacer { display: none; }
  .transport__row { justify-content: space-between; gap: 2px; }
  .iconbtn { width: 40px; height: 40px; }
  .iconbtn svg { width: 20px; height: 20px; }
  .iconbtn--labelled { min-width: 40px; height: 40px; }
  .iconbtn--play { width: 54px; height: 54px; }
  .iconbtn--play svg { width: 24px; height: 24px; }
  .timecode { min-width: 4ch; font-size: 11px; }

  /*
   * The volume popout is centred on its button, which sits at the end of the row
   * — centred there it would hang off the screen. Pinned to the inline end
   * instead, so it opens inwards.
   */
  .volume__popout { left: auto; right: 0; translate: 0; }
  .volume__panel input[type='range'] { width: 104px; }
}

.iconbtn--play {
  width: 62px;
  height: 62px;
  background: var(--accent);
  color: #000;
  box-shadow: 0 4px 22px color-mix(in srgb, var(--accent) 40%, transparent);
}
.iconbtn--play:hover { background: var(--accent); color: #000; filter: brightness(1.08); }
.iconbtn--play svg { width: 27px; height: 27px; }

/* The scrubber. */
.scrub {
  --progress: 0;
  --buffered: 0;
  position: relative;
  height: 22px;
  display: flex;
  align-items: center;
  cursor: pointer;
  touch-action: none;
}
.scrub__track {
  position: relative;
  width: 100%;
  height: 3px;
  border-radius: 99px;
  background: color-mix(in srgb, var(--ink) 16%, transparent);
  overflow: hidden;
}
.scrub__buffer {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: calc(var(--buffered) * 100%);
  background: color-mix(in srgb, var(--ink) 14%, transparent);
}
.scrub__fill {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: calc(var(--progress) * 100%);
  background: var(--accent);
}
.scrub__thumb {
  position: absolute;
  inset-inline-start: calc(var(--progress) * 100%);
  top: 50%;
  width: 12px;
  height: 12px;
  margin-inline-start: -6px;
  margin-top: -6px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 22%, transparent);
  opacity: 0;
  transition: opacity 140ms ease;
}
.scrub:hover .scrub__thumb, .scrub:focus-within .scrub__thumb { opacity: 1; }

/* ---------------------------------------------------------------------------
   Volume
   --------------------------------------------------------------------------- */

.volume { position: relative; display: flex; align-items: center; flex: none; }

/*
 * Opens upward, out of the transport. Anchored to the button rather than to the
 * bar so it stays with it at any width, and given a background of its own because
 * it sits over the scene.
 */
/*
 * The popout starts at the top edge of the button, not 8 px above it.
 *
 * That gap was fatal: it belonged to neither the button nor the popout, so moving
 * the mouse up towards the slider left the `.volume` element, fired `mouseleave`,
 * and closed the thing being reached for. The slider was visible and unreachable.
 *
 * `bottom: 100%` makes the two boxes touch, and the visual offset comes from
 * transparent padding *inside* the popout — so the pointer never crosses dead
 * space. The panel itself is the inner element.
 */
.volume__popout {
  position: absolute;
  bottom: 100%;
  left: 50%;
  translate: -50% 0;
  padding-bottom: 10px;
  z-index: 10;
}

.volume__panel {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-radius: var(--r-md);
  background: var(--panel-strong);
  border: 1px solid var(--panel-border);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: var(--shadow);
  white-space: nowrap;
}

.volume__panel input[type='range'] { width: 130px; }

.volume__value {
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--ink-faint);
  min-width: 4ch;
  text-align: end;
}

/* ---------------------------------------------------------------------------
   Library panel
   --------------------------------------------------------------------------- */

/* An explanatory line under a group heading — quieter than a setting's help
   text, because it describes the section rather than one control. */
.panel-note {
  padding: 0 var(--pad) 10px;
  font-size: 12px;
  line-height: 1.6;
  color: var(--ink-faint);
  max-width: 60ch;
}

/*
 * The empty state inside a section, rather than filling the whole sheet.
 *
 * Written as `.empty.empty--inline` on purpose. `.empty` carries `padding: 18vh`
 * and is defined further down this file; at equal specificity the later rule
 * wins, so a single-class override here was silently ignored and "Nothing saved
 * yet" appeared a third of the way down an otherwise blank panel.
 */
.empty.empty--inline {
  padding: 16px var(--pad) 24px;
  justify-items: start;
  text-align: start;
  font-size: 13px;
}

.timecode {
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--ink-faint);
  min-width: 5ch;
  flex: none;
}

/* ---------------------------------------------------------------------------
   Lists (reciters, surahs)
   --------------------------------------------------------------------------- */

/*
 * The dismiss layer behind a sheet. Covers the whole app, including the
 * transport, because the sheets are modal.
 */
/*
 * Above the banner (z-index 30), not below it.
 *
 * At 19 the scrim dimmed the player but left the install banner bright and
 * clickable over the top of it — and since the banner outranked the sheet too, it
 * would have drawn over a sheet they overlapped. A modal's scrim has to cover
 * everything the modal is covering.
 */
.sheet-scrim {
  position: absolute;
  inset: 0;
  z-index: 40;
  background: rgb(0 0 0 / 0.5);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  animation: scrim-in 200ms ease;
}
[data-theme='parchment'] .sheet-scrim { background: rgb(60 50 30 / 0.28); }
@keyframes scrim-in { from { opacity: 0; } }

.sheet {
  position: absolute;
  inset: 0;
  z-index: 41;
  display: grid;
  grid-template-rows: auto 1fr;
  background: var(--page);
  animation: sheet-in 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
@keyframes sheet-in { from { opacity: 0; transform: translateY(14px); } }

.sheet__head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: max(12px, env(safe-area-inset-top)) var(--pad) 12px;
  border-bottom: 1px solid var(--outline);
  background: var(--surface);
}

.sheet__title {
  font-family: var(--display-la);
  font-size: 17px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  margin: 0;
  flex: 1;
}
[dir='rtl'] .sheet__title {
  font-family: var(--display-ar);
  letter-spacing: normal;
  text-transform: none;
  font-size: 21px;
}

.sheet__body {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-bottom: max(24px, env(safe-area-inset-bottom));
}

.search {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  margin: 12px var(--pad);
  border-radius: var(--r-md);
  background: var(--surface-high);
  border: 1px solid var(--outline);
}
.search input { flex: 1; background: none; border: 0; outline: none; min-width: 0; }
.search svg { width: 18px; height: 18px; fill: var(--ink-faint); flex: none; }

.group-label {
  padding: 18px var(--pad) 8px;
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
[dir='rtl'] .group-label { letter-spacing: normal; text-transform: none; font-size: 13px; }

.row {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  padding: 12px var(--pad);
  text-align: start;
  transition: background 120ms ease;
  /* 155 reciter rows: let the browser skip layout for the ones off-screen. */
  content-visibility: auto;
  contain-intrinsic-size: auto 68px;
}
.row:hover { background: color-mix(in srgb, var(--ink) 6%, transparent); }
.row[aria-current='true'] { background: color-mix(in srgb, var(--accent) 12%, transparent); }

.row__mark {
  width: 42px;
  height: 42px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--row-accent, var(--accent)) 55%, transparent);
  color: var(--row-accent, var(--accent));
  font-family: var(--display-la);
  font-size: 14px;
  font-variant-numeric: tabular-nums;
}

.row__body { flex: 1; min-width: 0; display: grid; gap: 3px; }
.row__name {
  font-size: 15px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.row__name--ar { font-family: var(--display-ar); font-size: 18px; }
.row__sub {
  font-size: 12px;
  color: var(--ink-faint);
  display: flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
  overflow: hidden;
  min-width: 0;
}

/* The last item in a row's subtitle is the least important — the country — so it
   is the one allowed to run out of room, and it ellipses rather than being cut
   mid-letter, which reads as a rendering fault. */
.row__sub > :last-child {
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

/* ---------------------------------------------------------------------------
   Where saved audio lives: the storage notice in the Library
   --------------------------------------------------------------------------- */

.storage {
  margin: 4px var(--pad) 14px;
  padding: 14px 16px;
  border: 1px solid var(--panel-border);
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--ink) 4%, transparent);
  display: grid;
  gap: 12px;
}

.storage__head { display: flex; align-items: start; gap: 10px; }
.storage__icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  color: var(--ink-faint);
  margin-block-start: 1px;
}
.storage__icon svg { width: 16px; height: 16px; fill: currentColor; }
.storage__title { font-size: 13.5px; color: var(--ink); margin-block-end: 3px; }
.storage__body { font-size: 12px; line-height: 1.6; color: var(--ink-faint); }
.storage__body--warn { color: var(--ink-dim); margin-block-start: 6px; }

.storage__lines { display: grid; gap: 6px; }
.storage__line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  font-size: 12px;
  min-width: 0;
}
.storage__label { color: var(--ink-faint); flex: none; }
.storage__value { color: var(--ink-dim); min-width: 0; text-align: end; }

/*
 * A filesystem path is a Latin run and must not be reordered by an Arabic
 * interface, so it carries its own `dir` in the markup. Monospace because that is
 * what a path is, and it truncates rather than wrapping — the full text is in the
 * tooltip. It is there to answer "where?", not to be typed out.
 */
.storage__path {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  display: inline-block;
  max-width: 100%;
  vertical-align: bottom;
}

/*
 * The explanation above the button, always — not side by side.
 *
 * This first tried a row that stacked below 520px, and that was the wrong
 * measurement: the sheet is a fixed-width panel down one side, about 400px, so
 * the *viewport* is 1280px while the box holding this is narrow. The media query
 * never fired and the button, sized to its own text, squeezed the sentence beside
 * it into a four-word-tall column. Nothing here should be conditional on a width
 * this element does not have.
 */
.storage__state {
  display: grid;
  gap: 10px;
  justify-items: stretch;
  padding-block-start: 10px;
  border-block-start: 1px solid var(--panel-border);
}
.storage__state--ok {
  grid-auto-flow: column;
  grid-template-columns: auto 1fr;
  justify-items: start;
  align-items: start;
  color: var(--ink-dim);
  font-size: 12px;
}
.storage__state--ok .storage__icon { color: var(--accent); }
.storage__state [aria-disabled='true'] { opacity: 0.5; pointer-events: none; }

@media (max-width: 520px) {
  .storage__line { flex-direction: column; gap: 2px; }
  .storage__value { text-align: start; }
}

/* ---------------------------------------------------------------------------
   The translation picker: language groups, and the translations inside them
   --------------------------------------------------------------------------- */

/*
 * A language heading is a row that opens. It reads slightly heavier than the
 * translations under it so the two levels are distinguishable while scrolling
 * past sixty-six of them.
 */
.lang-row { gap: 8px; }
.lang-head { cursor: pointer; }
.lang-head .row__name { font-size: 16px; }

/*
 * The chevron turns rather than being swapped for a second glyph, so the state
 * and the drawing cannot get out of step. It is a physical rotation of a physical
 * arrow, so it is not mirrored in RTL — only its position in the row flips, which
 * `flex` already does.
 */
.lang-chev {
  flex: none;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  color: var(--ink-faint);
  transition: transform 160ms ease, color 140ms ease;
}
.lang-chev svg { width: 18px; height: 18px; fill: currentColor; }
.lang-chev--open { transform: rotate(180deg); color: var(--ink-dim); }
[data-reduced-motion='true'] .lang-chev { transition: none; }

/*
 * Translations sit indented under their language.
 *
 * `padding-inline-start` rather than `padding-left`: the indent has to move to
 * the right-hand side in an RTL interface or the hierarchy reads backwards.
 */
.lang-items { display: grid; }
.row--sub {
  padding-block: 9px;
  padding-inline-start: calc(var(--pad) + 18px);
  border-inline-start: 1px solid var(--panel-border);
  margin-inline-start: var(--pad);
  /* The parent already reserves height for the collapsed group; these rows are
     short and numerous, so a tighter intrinsic size keeps scrolling honest. */
  contain-intrinsic-size: auto 52px;
}
.row--sub .row__name { font-size: 14px; }

/* The tick marking the selected translation, at the end of its row. */
.row__check {
  flex: none;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  color: var(--accent);
}
.row__check svg { width: 18px; height: 18px; fill: currentColor; }

/* ---------------------------------------------------------------------------
   Split navigation button: one control, two actions
   --------------------------------------------------------------------------- */

/*
 * The translation control. The wide half toggles, the narrow half opens the
 * picker — see the note in src/ui/player.js for why they are split rather than
 * being one button that does both.
 *
 * The two halves share a border and a hover background so they read as one
 * control, with a hairline between them saying they are not one target.
 */
.navsplit {
  display: flex;
  align-items: center;
  flex: none;
  border-radius: 99px;
  border: 1px solid transparent;
  transition: border-color 140ms ease, background 140ms ease;
  max-width: 100%;
}
.navsplit:hover {
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  border-color: var(--panel-border);
}
/* The halves must not carry their own border or background, or the join shows. */
.navsplit .navbtn { border: 0; max-width: 100%; }
.navsplit .navbtn:hover { background: none; border-color: transparent; }
.navsplit .navbtn:focus-visible { border-radius: 99px; }

.navbtn--splitmain { padding-inline-end: 8px; }
.navbtn--splitend {
  padding-inline: 4px 9px;
  border-inline-start: 1px solid var(--panel-border) !important;
}
.navbtn__glyph--chev { width: 14px; height: 14px; }
.navbtn__glyph--chev svg { width: 14px; height: 14px; }

/*
 * Switched on: the accent, the same gold that marks the playing reciter. This is
 * the only signal that says whether the translation is showing without looking at
 * the caption, so it has to survive the label being dropped on a phone.
 */
.navsplit .navbtn[aria-checked='true'] { color: var(--accent); }
.navsplit[data-off='true'] .navbtn--splitmain { color: var(--ink-faint); }

/* Capability tags: word / ayah / no timing. */
.tag {
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: 99px;
  border: 1px solid currentColor;
  opacity: 0.75;
  flex: none;
}
[dir='rtl'] .tag { letter-spacing: normal; text-transform: none; font-size: 11px; }
.tag--word { color: var(--emerald); }
.tag--ayah { color: var(--gold-soft); }
.tag--none { color: var(--ink-faint); }

.star { flex: none; width: 38px; height: 38px; display: grid; place-items: center; border-radius: 50%; color: var(--ink-faint); }
.star svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.6; }
.star[aria-pressed='true'] { color: var(--gold-soft); }
.star[aria-pressed='true'] svg { fill: currentColor; }

/* ---------------------------------------------------------------------------
   Reading page — for the 115 reciters with no segmentation at all.
   --------------------------------------------------------------------------- */

.reading {
  position: relative;
  z-index: 2;
  height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: clamp(16px, 4vh, 40px) var(--pad);
  scroll-behavior: smooth;
}

.reading__ayah {
  font-family: var(--quran);
  direction: rtl;
  font-size: clamp(22px, 3.4vh, 34px);
  line-height: 2.1;
  color: var(--ink);
  padding: 10px 14px;
  border-radius: var(--r-md);
  max-width: 40ch;
  margin-inline: auto;
  scroll-margin-block: 30vh;
  transition: background 200ms ease;
  content-visibility: auto;
  contain-intrinsic-size: auto 120px;
}
.reading__ayah[aria-current='true'] { background: color-mix(in srgb, var(--accent) 14%, transparent); }
.reading__translation {
  max-width: 56ch;
  margin: 2px auto 20px;
  font-size: 14px;
  line-height: 1.6;
  color: var(--ink-faint);
  text-align: center;
}

/* ---------------------------------------------------------------------------
   Settings
   --------------------------------------------------------------------------- */

.settings-group { padding-block: 6px; }
.setting {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 12px var(--pad);
  min-height: 56px;
}
.setting__body { flex: 1; min-width: 0; }
.setting__label { font-size: 15px; }
.setting__help { font-size: 12px; color: var(--ink-faint); margin-top: 3px; line-height: 1.5; }

/*
 * The control column is capped as a share of the row, not of the viewport.
 *
 * `select.control` used to carry `max-width: 58vw`, which on a 1280 px window is
 * 742 px — inside a 460 px sheet. The translation picker, whose options are as
 * long as "Saheeh International — Saheeh International", took that width and
 * pushed its row 374 px past the edge of the panel while every other row ended
 * neatly. A viewport unit has no business sizing something inside a fixed-width
 * panel.
 */
.setting__control {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 58%;
  display: flex;
  justify-content: flex-end;
}

.switch {
  position: relative;
  width: 46px;
  height: 27px;
  border-radius: 99px;
  background: var(--outline);
  transition: background 160ms ease;
  flex: none;
}
.switch[aria-checked='true'] { background: var(--accent); }
.switch::after {
  content: '';
  position: absolute;
  top: 3px;
  inset-inline-start: 3px;
  width: 21px;
  height: 21px;
  border-radius: 50%;
  background: #fff;
  transition: translate 160ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.switch[aria-checked='true']::after { translate: 19px 0; }
[dir='rtl'] .switch[aria-checked='true']::after { translate: -19px 0; }

select.control, input.control {
  background: var(--surface-high);
  border: 1px solid var(--outline);
  border-radius: var(--r-sm);
  padding: 7px 10px;
  /* Bounded by the row, and allowed to shrink — a select with long option text
     truncates its own label rather than widening past the panel. */
  max-width: 100%;
  min-width: 0;
  text-overflow: ellipsis;
}

input[type='range'] {
  -webkit-appearance: none;
  appearance: none;
  width: min(200px, 42vw);
  height: 3px;
  border-radius: 99px;
  background: var(--outline);
}
input[type='range']::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--accent);
  cursor: pointer;
}
input[type='range']::-moz-range-thumb {
  width: 16px; height: 16px; border: 0; border-radius: 50%; background: var(--accent); cursor: pointer;
}

.segmented {
  display: inline-flex;
  border: 1px solid var(--outline);
  border-radius: var(--r-sm);
  overflow: hidden;
}
.segmented button { padding: 7px 12px; font-size: 13px; color: var(--ink-dim); }
.segmented button[aria-pressed='true'] { background: var(--accent); color: #000; }

/* ---------------------------------------------------------------------------
   Chrome, banners, misc
   --------------------------------------------------------------------------- */

.topbar {
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 6;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: max(10px, env(safe-area-inset-top)) var(--pad) 10px;
}

.credit { flex: 1; min-width: 0; display: grid; gap: 2px; }
.credit__name {
  font-size: 13px;
  color: var(--ink-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.credit__name--ar { font-family: var(--display-ar); font-size: 16px; }
.credit__sub { font-size: 11px; color: var(--ink-faint); }

.banner {
  position: absolute;
  /*
   * Pinned to the bottom of the *view*, which is already the row above the
   * transport — so no allowance for the transport's height is needed here. It
   * previously subtracted one, which floated the banner into the middle of the
   * screen directly over the ayah.
   */
  inset-block-end: 14px;
  inset-inline: 0;
  margin-inline: auto;
  width: fit-content;
  max-width: min(560px, calc(100% - var(--pad) * 2));
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  border-radius: var(--r-md);
  background: var(--panel-strong);
  border: 1px solid var(--panel-border);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: var(--shadow);
  font-size: 13px;
  animation: sheet-in 200ms ease;
}
.banner--error { border-color: color-mix(in srgb, #e57373 50%, transparent); }
.banner__action { color: var(--accent); font-weight: 600; flex: none; }

.spinner {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--ink) 22%, transparent);
  border-top-color: var(--accent);
  animation: spin 700ms linear infinite;
  flex: none;
}
@keyframes spin { to { rotate: 360deg; } }

.empty {
  display: grid;
  justify-items: center;
  gap: 10px;
  padding: 18vh var(--pad);
  color: var(--ink-faint);
  text-align: center;
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
   Layout at width
   --------------------------------------------------------------------------- */

@media (min-width: 900px) {
  :root { --transport-h: 104px; }
  .sheet { inset-inline-start: auto; width: min(460px, 42vw); border-inline-start: 1px solid var(--outline); box-shadow: var(--shadow); }
  [dir='rtl'] .sheet { inset-inline-end: auto; inset-inline-start: 0; }
}

@media (max-height: 520px) {
  .plaque { padding-block: 6px; }
  .plaque__glyph { font-size: clamp(28px, 6vh, 44px); }
}
