/* ==========================================================================
   雙語敬拜詩歌 — shared styles
   Light/dark themes via CSS custom properties.
   Theme resolution: explicit user choice sets data-theme on <html>;
   otherwise prefers-color-scheme decides.
   ========================================================================== */

/* Display typeface (DEC-1). Self-hosted rather than pulled from a font CDN:
   this site deploys as committed static assets with no build step, and a
   third-party request is one more thing that can be slow or blocked in the
   room where it is being projected.

   One variable file, latin subset, weights 500-700 -- 66 KB, which is less
   than the two static weights it replaces would have been. Latin only on
   purpose: it is what Fraunces has, and CJK display text is served by the
   system serifs in --font-display below.

   swap, not block: a title that arrives a moment late in Georgia is better
   than a title that is not there at all. */
@font-face {
  font-family: "Fraunces";
  font-style: normal;
  font-weight: 500 700;
  font-display: swap;
  src: url("../fonts/fraunces-latin-var.woff2") format("woff2");
  /* The subset the file actually carries. Stating it keeps the browser from
     downloading 66 KB to render a CJK title it has no glyph for. */
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Typography tokens live apart from the palette blocks below, which are
   colour-only and restated per theme -- a font does not change with the
   lights. Latin falls to Fraunces, then Georgia; CJK skips both (no glyphs)
   and lands on a serif, so a Chinese or Korean title reads as classical
   alongside an English one rather than reverting to the body sans.

   The CJK half is chosen per language rather than stacked in one list. A font
   stack resolves per character, not per language, so a single list hands the
   first face that has the glyph -- and Simplified and Traditional Chinese
   share codepoints whose correct shapes differ. With one list, 简体 titles
   render in Songti TC, a Traditional face (verified with CDP glyph counts on
   macOS 15); split by :lang() they get Songti SC. Korean resolves the same
   either way here, since Fraunces and Georgia have no Hangul and the generic
   serif finds AppleMyungjo -- the split spells that out rather than leaving
   it to whatever a given platform maps `serif` to.

   js/i18n.js keeps <html lang> in step with the reader's choice, so these
   switch with the language picker. */
:root {
  --font-display: "Fraunces", Georgia, serif;
}

:root:lang(zh-Hant) {
  --font-display: "Fraunces", Georgia, "Songti TC", "Noto Serif TC", serif;
}

:root:lang(zh-Hans) {
  --font-display: "Fraunces", Georgia, "Songti SC", "Noto Serif SC", serif;
}

/* Noto first where it is installed (Linux, Android, anyone who added it),
   then the faces macOS and Windows ship by default, so the criterion holds
   without asking the reader to install anything. */
:root:lang(ko) {
  --font-display: "Fraunces", Georgia, "Noto Serif KR", AppleMyungjo, Batang,
    serif;
}

/* Light palette: the default, and what an explicit light choice resolves to.
   Listing both selectors here rather than repeating the values in a separate
   [data-theme="light"] block keeps one source of truth for the light theme. */
:root,
:root[data-theme="light"] {
  --bg: #faf8f3;
  --bg-elev: #ffffff;
  --bg-inset: #f1ecdf;
  --text: #2b251d;
  --text-soft: #6f6557;
  --accent: #8a6420;
  --accent-strong: #6f4f16;
  --accent-tint: rgba(138, 100, 32, 0.10);
  --border: #e2d9c8;
  --shadow: 0 1px 3px rgba(43, 37, 29, 0.08), 0 4px 16px rgba(43, 37, 29, 0.05);
  --ok: #3c7a3f;
  --ok-tint: rgba(60, 122, 63, 0.12);
  --focus: #8a6420;
  --lyrics-scale: 1;
}

/* The dark palette is stated twice on purpose: once for "system says dark and
   the user has not overridden to light", once for "the user explicitly chose
   dark". A media query is a condition, not a selector, so plain CSS cannot
   merge the two into one rule. The values below must be kept in sync.
   (light-dark() would collapse both into one declaration. It is not used here
   because on a browser that lacks it every declaration consuming a wrapped
   token — background, color, border — becomes invalid at computed-value time
   and falls back to its initial value. The custom properties themselves
   survive, but since the whole palette would be wrapped, the page still loses
   its colours. Not worth that risk for a site projected on unknown hardware.) */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #171310;
    --bg-elev: #211c16;
    --bg-inset: #2a241c;
    --text: #ece4d6;
    --text-soft: #a5998a;
    --accent: #d5a852;
    --accent-strong: #e4bd6f;
    --accent-tint: rgba(213, 168, 82, 0.14);
    --border: #383024;
    --shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.3);
    --ok: #7cbf80;
    --ok-tint: rgba(124, 191, 128, 0.16);
    --focus: #d5a852;
  }
}

:root[data-theme="dark"] {
  --bg: #171310;
  --bg-elev: #211c16;
  --bg-inset: #2a241c;
  --text: #ece4d6;
  --text-soft: #a5998a;
  --accent: #d5a852;
  --accent-strong: #e4bd6f;
  --accent-tint: rgba(213, 168, 82, 0.14);
  --border: #383024;
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.3);
  --ok: #7cbf80;
  --ok-tint: rgba(124, 191, 128, 0.16);
  --focus: #d5a852;
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

/* App shell: the page is a fixed-viewport column of pinned bands — topbar,
   page head, footer — around one scrollable middle (main.scroll). 100vh is
   the fallback for browsers without dynamic-viewport units; 100dvh tracks
   the real visible height when mobile browser chrome shows and hides. */
body {
  margin: 0;
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, "PingFang TC", "Microsoft JhengHei",
    "Noto Sans TC", "Noto Sans CJK TC", sans-serif;
  line-height: 1.6;
  transition: background-color 0.25s ease, color 0.25s ease;
}

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

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
}

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

/* Read aloud, not shown. For information a sighted reader gets from a picture
   -- the lyric-language flags -- which a screen reader would otherwise
   announce as a list of countries. clip-path rather than display:none or
   visibility:hidden, both of which take it out of the accessibility tree too. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.container {
  max-width: 72rem;
  margin: 0 auto;
  padding: 1rem 1rem 2rem;
}

/* --------------------------------------------------------------------------
   App shell bands
   -------------------------------------------------------------------------- */
/* The per-page attention block pinned under the topbar: page title and
   search on the index, song title and controls on the song page. */
.page-head {
  flex: none;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}

.page-head-inner {
  max-width: 72rem;
  margin: 0 auto;
  padding: 0 1rem;
}

/* The one scrollable region. min-height: 0 lets the flex item actually
   shrink below its content, which is what makes the overflow scroll. */
.scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

/* Pinned bottom band: the song page's prev/next nav, then the footer.
   The border sits on the children, not the band, so a page whose footer is
   hidden (no config) does not show a stray rule above nothing. */
.page-foot {
  flex: none;
  background: var(--bg);
}

.page-foot > * { border-top: 1px solid var(--border); }

/* --------------------------------------------------------------------------
   Top bar
   -------------------------------------------------------------------------- */
.topbar {
  flex: none;
  position: relative; /* keeps the language menu stacked over the bands below */
  z-index: 20;
  background: var(--bg); /* fallback when color-mix is unsupported */
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
}

.topbar-inner {
  max-width: 72rem;
  margin: 0 auto;
  padding: 0.6rem 1rem;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}

/* Display role. The tracking is tighter than it was under the system sans:
   Fraunces is drawn with its own generous sidebearings, and the 0.04em that
   opened up the sans pulls a serif apart. */
.site-title {
  font-family: var(--font-display);
  font-size: 1.2rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  text-decoration: none;
  color: var(--text);
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}

.site-title .en {
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--text-soft);
  letter-spacing: 0.02em;
}

.spacer { flex: 1; }

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  min-width: 2.25rem;
  height: 2.25rem;
  padding: 0 0.6rem;
  border-radius: 0.6rem;
  border: 1px solid var(--border);
  background: var(--bg-elev);
  color: var(--text);
  font-size: 0.9rem;
  transition: border-color 0.15s ease, background-color 0.15s ease;
}

.icon-btn:hover { border-color: var(--accent); }

.icon-btn svg { width: 1.15rem; height: 1.15rem; fill: currentColor; }

/* SVG has no UA [hidden] rule in all browsers — enforce it. */
.icon-btn svg[hidden] { display: none; }

/* An author-origin `display` outranks the user-agent [hidden] rule, so an
   element that declares one stays laid out — and clickable — when JavaScript
   sets the attribute. Every element this site hides that way restates it
   here: the mode buttons and the language switcher, and the controls bar and
   mini player, which are markup-hidden until a song decides they apply. */
.seg[hidden],
.lang-switcher[hidden],
.controls[hidden],
.pager[hidden],
.miniplayer[hidden] { display: none; }

/* --------------------------------------------------------------------------
   Language switcher and first-visit picker
   -------------------------------------------------------------------------- */
.lang-switcher { position: relative; display: inline-flex; }

/* The face is one flag (DEC-2), so the button is a square the same size as
   the theme toggle beside it and does not resize between languages. The flag
   is the whole content, so it gets the room the code used to take. */
.lang-btn {
  padding: 0;
  font-size: 1.15rem;
  line-height: 1;
}

.lang-menu {
  position: absolute;
  top: calc(100% + 0.4rem);
  right: 0;
  z-index: 30;
  min-width: 12rem;
  padding: 0.3rem;
  border: 1px solid var(--border);
  border-radius: 0.7rem;
  background: var(--bg-elev);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18);
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.lang-item {
  display: flex;
  align-items: baseline;
  gap: 0.55rem;
  width: 100%;
  padding: 0.5rem 0.6rem;
  border: none;
  border-radius: 0.5rem;
  background: none;
  color: var(--text);
  font-size: 0.95rem;
  text-align: left;
}

.lang-item:hover,
.lang-item:focus-visible { background: var(--bg-inset); }
.lang-item.current { background: var(--accent-tint); }
.lang-item .lang-flag { font-size: 1.1em; }
/* The native name needs no alignment rule of its own: with the code column
   gone (DEC-2) a row is flag then name, which is what the flex default
   already does. */

/* DEC-A: a centered modal card rather than a banner. A banner can be
   dismissed without choosing, which strands exactly the readers it exists
   for on the base language with no sign a choice was ever offered. */
.lang-picker {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.25rem;
  background: rgba(0, 0, 0, 0.55);
}

body.has-picker { overflow: hidden; }

.lang-picker-card {
  width: min(26rem, 100%);
  padding: 1.6rem 1.5rem;
  border: 1px solid var(--border);
  border-radius: 1rem;
  background: var(--bg-elev);
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
}

.lang-picker-card h2 { margin: 0 0 0.5rem; font-size: 1.2rem; }
.lang-picker-card p {
  margin: 0 0 1.1rem;
  color: var(--text-soft);
  font-size: 0.95rem;
}

.lang-picker-options { display: flex; flex-direction: column; gap: 0.4rem; }
.lang-picker-options .lang-item {
  border: 1px solid var(--border);
  padding: 0.7rem 0.8rem;
  font-size: 1rem;
}

/* --------------------------------------------------------------------------
   Index page — song list
   -------------------------------------------------------------------------- */
/* Pinned in the page head, so the paddings are a fraction of what the old
   in-flow hero used: every rem here is stolen from the scroll area below. */
.hero {
  padding: 0.9rem 0 0.35rem;
}

.hero h1 {
  margin: 0 0 0.15rem;
  font-family: var(--font-display);
  font-size: clamp(1.55rem, 4vw, 2.05rem);
  font-weight: 600;
  letter-spacing: 0.005em;
}

.hero p {
  margin: 0;
  color: var(--text-soft);
  font-size: 0.9rem;
}

/* On a short viewport the tagline is the least load-bearing pinned line. */
@media (max-height: 640px) {
  .hero p { display: none; }
}

.search-row {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  margin: 0.4rem 0 0.5rem;
}

.search-input {
  flex: 1;
  font: inherit;
  color: var(--text);
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 0.75rem;
  padding: 0.65rem 1rem;
  box-shadow: var(--shadow);
}

.search-input::placeholder { color: var(--text-soft); }

.search-input:focus {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
  border-color: transparent;
}

.result-count {
  color: var(--text-soft);
  font-size: 0.8rem;
  margin: 0 0 0.5rem 0.25rem;
}

.song-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
  gap: 0.7rem;
}

/* Every card is the same height (DEC-4). The grid already stretches cards to
   match within a row; what made rows differ was the title, which is one line
   for most songs and two for the long ones (047 in English). So the title
   block reserves two lines' worth of space whether or not it needs them, and
   the badges are pushed to the bottom of the column -- giving every card the
   same intrinsic height, in every language, without a fixed card height that
   would break when the text scales. */
.song-card {
  display: flex;
  flex-direction: column;
  /* The reserve below is computed from these, so the two cannot drift apart.
     They live on the card, not on .pref-title, so the untitled variant can
     shrink its own font without shrinking the space it holds. */
  --card-title-size: 1.15rem;
  --card-title-leading: 1.4;
  text-decoration: none;
  color: var(--text);
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 0.85rem;
  padding: 0.8rem 1rem;
  box-shadow: var(--shadow);
  transition: border-color 0.15s ease, transform 0.15s ease;
}

.song-card:hover {
  border-color: var(--accent);
  transform: translateY(-1px);
}

.song-card .num {
  color: var(--text-soft);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
}

/* Two lines, always: clamped so a longer title cannot push the card taller,
   min-height so a shorter one cannot let it shrink. -webkit-line-clamp is the
   prefixed spelling every current browser implements, including Firefox; a
   browser without it just shows the full title and loses the equal heights,
   which is the old behaviour rather than a broken one. */
.song-card .pref-title {
  font-size: var(--card-title-size);
  font-weight: 600;
  line-height: var(--card-title-leading);
  min-height: calc(var(--card-title-size) * var(--card-title-leading) * 2);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Smaller type, same reserved box: min-height above is in rem off the card's
   token, not this element's font-size, so the placeholder holds the same
   space a real title would. */
.song-card .pref-title.untitled {
  color: var(--text-soft);
  font-weight: 400;
  font-size: 0.95rem;
}

/* One line. Absent entirely for a reader on the base language -- there is no
   second title to show -- which is consistent within a language, and language
   is what the cards are compared across. */
.song-card .base-title {
  color: var(--text-soft);
  font-size: 0.9rem;
  line-height: 1.4;
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* auto pushes the badges to the bottom of the card's column, so they line up
   across a row however tall the card ends up.

   nowrap, deliberately: the badges are the last thing in the card, so a badge
   row that wrapped to two lines added its height to that card and only that
   card -- undoing the uniform sizing the title reserve above exists to
   protect. It showed up at 320px and at a large browser font size, where the
   cards carrying two video badges grew past their neighbours. */
.badges {
  margin-top: auto;
  padding-top: 0.5rem;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.4rem;
}

/* With no wrapping to absorb the overflow, something has to give when the card
   is too narrow for both badges. It is the video badge: the lyrics badge is
   what the reader came to the list for, so it keeps its full width and the
   video badge ellipsises. */
.badge { 
  flex: none; 
  white-space: nowrap; 
  max-width: 100%;
}

.badge.ok {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.badge {
  font-size: 0.72rem;
  padding: 0.1rem 0.5rem;
  border-radius: 1rem;
  border: 1px solid var(--border);
  color: var(--text-soft);
}

.badge.ok {
  color: var(--ok);
  background: var(--ok-tint);
  border-color: transparent;
}

.badge.accent {
  color: var(--accent-strong);
  background: var(--accent-tint);
  border-color: transparent;
}

/* Flags are pictures, not text: at the badge's 0.72rem two red flags are the
   same red smudge. Bigger, with room between them, and the line-height pinned
   so the taller glyphs do not grow the badge and with it the card.

   The gap after the label lives here rather than in the JS, because the label
   ends in a full-width colon in Chinese (歌詞：) and an ASCII one in English
   and Korean; a literal space in the string reads as a double gap after the
   full-width form. */
.badge-flags {
  margin-inline-start: 0.25em;
  font-size: 1.25em;
  letter-spacing: 0.08em;
  line-height: 1;
  vertical-align: -0.1em;
}

.empty-note {
  text-align: center;
  color: var(--text-soft);
  padding: 3rem 1rem;
}

/* --------------------------------------------------------------------------
   Song list pager
   -------------------------------------------------------------------------- */
/* In the scroll area after the list, never pinned: it is where you arrive
   having read the page, not something to keep in view while reading it. */
.pager {
  max-width: 72rem;
  margin: 0 auto;
  padding: 0.6rem 1rem;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.pager-btn {
  min-width: 2.25rem;
  height: 2.25rem;
  padding: 0 0.6rem;
  border-radius: 0.6rem;
  border: 1px solid var(--border);
  background: var(--bg-elev);
  color: var(--text);
  font-size: 0.9rem;
  /* Tabular figures so the row does not shift as the digits change. */
  font-variant-numeric: tabular-nums;
  transition: border-color 0.15s ease, background-color 0.15s ease;
}

/* :not([aria-current]) rather than relying on source order -- both selectors
   set border-color at the same specificity, so hovering the current page would
   otherwise restore the accent border the rule below deliberately removes, and
   offer a click that does nothing. */
.pager-btn:hover:not(:disabled):not([aria-current]) { border-color: var(--accent); }

/* The current page is a button that goes nowhere, so it reads as a state
   rather than an offer -- no border, no hover, and aria-current says the
   same thing to a screen reader. */
.pager-btn[aria-current="page"] {
  background: var(--accent-tint);
  border-color: transparent;
  color: var(--accent-strong);
  font-weight: 600;
  cursor: default;
}

.pager-btn:disabled {
  opacity: 0.45;
  cursor: default;
}

.pager-gap {
  color: var(--text-soft);
  padding: 0 0.1rem;
}

.help {
  margin-top: 0.5rem;
  border: 1px solid var(--border);
  border-radius: 0.85rem;
  background: var(--bg-elev);
  padding: 0.25rem 1rem;
}

.help summary {
  cursor: pointer;
  padding: 0.6rem 0;
  font-weight: 600;
}

.help ul { color: var(--text-soft); padding-left: 1.2rem; }

/* --------------------------------------------------------------------------
   History page — what changed, newest first
   -------------------------------------------------------------------------- */
.history {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.version {
  border: 1px solid var(--border);
  border-radius: 0.85rem;
  background: var(--bg-elev);
  padding: 1rem 1.2rem;
  box-shadow: var(--shadow);
}

/* The newest release is the one the reader is running, so it is the one the
   eye should land on first. */
.version.current {
  border-color: var(--accent);
}

.version-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.version-no {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: 0.005em;
}

/* Pushed to the far end, so a row of headings lines its dates up however long
   the version numbers get. */
.version-date {
  margin-left: auto;
  color: var(--text-soft);
  font-size: 0.8rem;
}

.version-changes {
  margin: 0.6rem 0 0;
  padding-left: 1.2rem;
  color: var(--text-soft);
}

.version-changes li { margin-bottom: 0.3rem; }
.version-changes li:last-child { margin-bottom: 0; }

/* Pinned single line at the bottom of every page. */
.footer {
  color: var(--text-soft);
  font-size: 0.75rem;
  text-align: center;
}

.footer p {
  max-width: 72rem;
  margin: 0 auto;
  padding: 0.4rem 1rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.footer a { color: inherit; }

/* --------------------------------------------------------------------------
   Song page — header & controls
   -------------------------------------------------------------------------- */
.song-head {
  padding: 0.8rem 0 0.35rem;
}

/* Both lines are the song's name, so both are display type -- .zh carries
   whichever language the reader chose (often CJK, which the stack hands to a
   system serif) and .en the English underneath. */
.song-head .zh {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.45rem, 4vw, 1.95rem);
  font-weight: 600;
  line-height: 1.3;
}

.song-head .en {
  margin: 0.1rem 0 0;
  font-family: var(--font-display);
  color: var(--text-soft);
  font-size: clamp(0.95rem, 2.5vw, 1.1rem);
  font-weight: 500;
}

/* Lives in the pinned page head now, so no sticky offset to keep in sync
   with the topbar height and no translucent backdrop of its own. */
.controls {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding: 0.15rem 0 0.7rem;
}

.seg {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: 0.6rem;
  overflow: hidden;
  background: var(--bg-elev);
}

.seg button {
  padding: 0.4rem 0.8rem;
  font-size: 0.88rem;
  color: var(--text-soft);
  border-right: 1px solid var(--border);
}

.seg button:last-child { border-right: none; }

.seg button[aria-pressed="true"] {
  background: var(--accent-tint);
  color: var(--accent-strong);
  font-weight: 600;
}

.font-controls {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
  border: 1px solid var(--border);
  border-radius: 0.6rem;
  background: var(--bg-elev);
  overflow: hidden;
}

.font-controls button {
  padding: 0.4rem 0.7rem;
  color: var(--text-soft);
}

.font-controls button:hover { color: var(--accent-strong); }

/* --------------------------------------------------------------------------
   Mini player bar (audio-first; video enlargeable)
   -------------------------------------------------------------------------- */
.miniplayer {
  /* Floats above the pinned .page-foot (song-nav + footer); the expanded
     video position below derives from the same value. */
  --mp-bottom: 6.4rem;
  position: fixed;
  left: 50%;
  bottom: var(--mp-bottom);
  transform: translateX(-50%);
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 0.7rem;
  width: min(34rem, calc(100vw - 1.5rem));
  padding: 0.5rem 0.7rem;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 1rem;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18), var(--shadow);
}

.mp-video {
  position: relative;
  flex: none;
  width: 7rem;
  aspect-ratio: 16 / 9;
  border-radius: 0.5rem;
  overflow: hidden;
  background: #000;
  transition: width 0.2s ease;
}

.mp-video iframe,
.mp-video img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
}

.mp-video .mp-note {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 0.62rem;
  text-align: center;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
  background: rgba(0, 0, 0, 0.25);
  pointer-events: none;
}

/* Enlarged: video docks above the bar as a big panel; same iframe keeps playing. */
.miniplayer.expanded .mp-video {
  position: fixed;
  left: 50%;
  bottom: calc(var(--mp-bottom) + 4.5rem);
  transform: translateX(-50%);
  width: min(52rem, calc(100vw - 1.5rem));
  border-radius: 0.85rem;
  border: 1px solid var(--border);
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
}

.mp-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.mp-title {
  font-size: 0.85rem;
  font-weight: 600;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: normal;
}

.mp-source-select {
  font: inherit;
  font-size: 0.75rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 0.3rem;
  padding: 0.1rem 0.3rem;
  max-width: 100%;
}

.mp-tabs {
  display: flex;
  gap: 0.3rem;
  flex-wrap: wrap;
}

.mp-tabs .tab {
  font-size: 0.72rem;
  padding: 0.08rem 0.55rem;
  border-radius: 1rem;
  border: 1px solid var(--border);
  color: var(--text-soft);
}

.mp-tabs .tab[aria-selected="true"] {
  background: var(--accent-tint);
  color: var(--accent-strong);
  border-color: transparent;
  font-weight: 600;
}

.mp-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.6rem;
  height: 2.6rem;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  transition: border-color 0.15s ease, color 0.15s ease;
}

.mp-btn:hover { border-color: var(--accent); color: var(--accent-strong); }

.mp-btn svg { width: 1.3rem; height: 1.3rem; fill: currentColor; }

/* SVG has no UA [hidden] rule in all browsers — enforce it. */
.mp-btn svg[hidden] { display: none; }

#mp-play {
  background: var(--accent-tint);
  color: var(--accent-strong);
  border-color: transparent;
}

/* Room for the floating bar over the scroll area's last lines */
body.has-miniplayer .scroll .container { padding-bottom: 5.5rem; }

/* --------------------------------------------------------------------------
   Lyrics
   -------------------------------------------------------------------------- */
.lyrics {
  font-size: calc(1.18rem * var(--lyrics-scale));
  padding-bottom: 1rem;
}

.lyrics .section { margin: 0 0 2.2em; }

.section-label {
  font-size: 0.72em;
  font-weight: 600;
  letter-spacing: 0.1em;
  color: var(--accent-strong);
  text-transform: uppercase;
  margin: 0 0 0.7em;
  padding-bottom: 0.25em;
  border-bottom: 1px solid var(--border);
}

.line-base { color: var(--text); }
.line-pref { color: var(--text); font-weight: 500; }

/* Mode: side-by-side (並排對照) */
.mode-side .pair-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 2.5em;
  row-gap: 0.45em;
  align-items: baseline;
}

/* Mode: stacked (上下對照) */
.mode-stack .lang-block { margin-bottom: 1em; }
.mode-stack .lang-block > div { margin-bottom: 0.35em; }
.mode-stack .lang-block.pref-block {
  padding-left: 0.9em;
  border-left: 3px solid var(--accent-tint);
}

/* Mode: alternating lines (逐句對照) */
.mode-alt .pair { margin-bottom: 0.9em; }
.mode-alt .pair .line-base {
  color: var(--text-soft);
  font-size: 0.9em;
}

/* Mode: single column — the song carries nothing in the reader's language,
   or that language is the base one. Either way there is no pair to arrange. */
.mode-single .lang-block > div { margin-bottom: 0.35em; }

.lyrics-notice {
  font-size: 0.62em;
  color: var(--text-soft);
  background: var(--bg-inset);
  border-radius: 0.6rem;
  padding: 0.7em 0.9em;
  margin: 0 0 1.6em;
}

.lyrics-pending {
  text-align: center;
  color: var(--text-soft);
  background: var(--bg-inset);
  border-radius: 0.85rem;
  padding: 3rem 1.5rem;
  margin: 1rem 0;
}

.lyrics-pending .big { font-size: 1.3rem; margin-bottom: 0.4rem; }

/* Prev / next navigation — pinned above the footer in .page-foot. */
.song-nav {
  max-width: 72rem;
  margin: 0 auto;
  padding: 0.45rem 1rem;
  display: flex;
  justify-content: space-between;
  gap: 1rem;
}

.song-nav a {
  text-decoration: none;
  color: var(--text-soft);
  max-width: 45%;
  min-width: 0;
}

.song-nav a:hover { color: var(--accent-strong); }

.song-nav .dir {
  font-size: 0.7rem;
  letter-spacing: 0.08em;
}

.song-nav .t {
  font-weight: 600;
  color: var(--text);
  font-size: 0.9rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.song-nav a:hover .t { color: var(--accent-strong); }

.song-nav .next { text-align: right; margin-left: auto; }

/* --------------------------------------------------------------------------
   Responsive
   -------------------------------------------------------------------------- */
@media (max-width: 700px) {
  .song-list { grid-template-columns: 1fr; }
  .mp-video { width: 5.2rem; }
}

/* --------------------------------------------------------------------------
   Print — lyrics sheet only
   -------------------------------------------------------------------------- */
@media print {
  .topbar, .controls, .miniplayer, .song-nav, .page-foot, .help, .footer,
  .lang-picker, .lyrics-notice, .pager { display: none !important; }
  /* Undo the fixed-viewport shell: print needs the document to flow across
     pages, not scroll inside one. */
  body {
    display: block;
    height: auto;
    overflow: visible;
    background: #fff;
    color: #000;
  }
  .scroll { overflow: visible; }
  .page-head { border-bottom: none; }
  .lyrics { font-size: 12pt; }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
}
