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

/* Site typeface. 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, full wght and opsz axes -- 66 KB. The
   optical-size axis is what lets one face serve titles and body text alike:
   browsers apply it automatically (font-optical-sizing defaults to auto), so
   lyrics and prose render the 9pt text grade while large titles render the
   display grade. Latin only on purpose: it is what Fraunces has, and CJK
   text is served by the system serifs in --font-display below, with the
   self-hosted Noto Serif subsets after them as the cross-platform floor.

   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: 100 900;
  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;
}

/* CJK serif subsets, self-hosted for the platforms that ship no CJK serif a
   web page can name: iOS (Songti and AppleMyungjo are macOS faces), Windows
   (SimSun/Batang render spindly at text sizes) and Android. Built by
   scripts/subset_cjk_fonts.py from the characters the site's data actually
   uses (~400 KB per Chinese script, ~180 KB Korean, vs ~20 MB full), wght
   axis kept at 400-700 so titles get real bold, not synthetic CJK smearing.
   validate.py fails the gate if site text ever uses a character the subsets
   do not carry.

   They sit AFTER the system serifs in the per-language stacks below, so a
   Mac never downloads them; the unicode-range means only CJK text triggers
   the download at all. A character missing from one face falls through to
   the next per glyph — that is why each stack ends with the other scripts'
   subsets (e.g. 简 exists only in the SC file). */
@font-face {
  font-family: "Noto Serif TC";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../fonts/noto-serif-tc-sub.woff2") format("woff2");
  unicode-range: U+1100-11FF, U+2E80-9FFF, U+AC00-D7AF, U+F900-FAFF,
    U+FE30-FE4F, U+FF00-FFEF;
}

@font-face {
  font-family: "Noto Serif SC";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../fonts/noto-serif-sc-sub.woff2") format("woff2");
  unicode-range: U+1100-11FF, U+2E80-9FFF, U+AC00-D7AF, U+F900-FAFF,
    U+FE30-FE4F, U+FF00-FFEF;
}

@font-face {
  font-family: "Noto Serif KR";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../fonts/noto-serif-kr-sub.woff2") format("woff2");
  unicode-range: U+1100-11FF, U+2E80-9FFF, U+AC00-D7AF, U+F900-FAFF,
    U+FE30-FE4F, U+FF00-FFEF;
}

/* 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.

   Two voices (owner decision, 2026-08-19): --font-display is the reading
   voice -- body inherits it, so lyrics, song titles, cards and prose all
   share the title face rather than splitting serif titles over a sans page.
   --font-ui is the functional chrome -- buttons, form controls, badges, the
   mini player, the footer -- kept on the system sans, because chrome is
   where the sub-0.8rem CJK text lives and small CJK serifs (SimSun, Batang)
   go spindly on the low-DPI hardware this site gets projected from.

   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 a 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. The Noto Serif names now
   resolve to the self-hosted subsets above, so every platform lands on a
   real serif; the system faces stay ahead of them so a Mac downloads
   nothing. Each stack ends with the other scripts' subsets for the stray
   cross-script characters site text carries (the changelog names 简体中文
   and 한국어 in every language).

   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, "Noto Serif TC", "Noto Serif SC",
    "Noto Serif KR", serif;
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, "PingFang TC", "Microsoft JhengHei",
    "Noto Sans TC", "Noto Sans CJK TC", sans-serif;
}

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

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

/* The subset intercepts ahead of AppleMyungjo/Batang: Noto Serif KR is the
   better face, and 180 KB once (cached) buys the same rendering on every
   platform. The system names remain for readers with the webfont blocked. */
:root:lang(ko) {
  --font-display: "Fraunces", Georgia, "Noto Serif KR", AppleMyungjo, Batang,
    "Noto Serif TC", "Noto Serif SC", 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);
  /* The reading voice, site-wide; chrome opts back into --font-ui below. */
  font-family: var(--font-display);
  line-height: 1.6;
  transition: background-color 0.25s ease, color 0.25s ease;
}

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

button {
  font: inherit;
  font-family: var(--font-ui); /* controls are chrome, not reading text */
  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;
}

/* 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-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;
}

/* Pinned page-head hero, used by help.html and history.html -- pages whose
   identity ("How to use", "What's new") is not in the banner. The song list
   no longer uses it (v2.2, DEC-P2): there the banner names the site, the h1
   is screen-reader-only, and the search box leads the pinned head. */
.hero {
  padding: 0.9rem 0 0.35rem;
}

.hero h1 {
  margin: 0 0 0.15rem;
  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; }
}

/* --------------------------------------------------------------------------
   Index page — song list
   -------------------------------------------------------------------------- */
.search-row {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  margin: 0.8rem 0 0.5rem;
}

.search-input {
  flex: 1;
  font: inherit;
  font-family: var(--font-ui);
  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;
}

/* Cards hug their content (v2.2, superseding v2.1's two-line reserve): a
   one-line title holds no empty band under it, which is most of the height a
   mobile single-column list was wasting. Within a desktop grid row the grid
   still stretches every card to the row's tallest, and margin-top: auto on
   .badges keeps the badge rows bottom-aligned across that row; only across
   rows -- and on the one-column mobile layout -- may heights now differ. */
.song-card {
  display: flex;
  flex-direction: column;
  /* The grid stretches the <li>; the card must fill it, or two cards in one
     row with different content would show different heights and the badges'
     margin-top: auto would have nothing to push against. */
  height: 100%;
  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;
}

/* At most two lines: clamped so a longer title cannot run the card long, but
   no reserved minimum -- a one-line title takes one line. -webkit-line-clamp
   is the prefixed spelling every current browser implements, including
   Firefox; a browser without it just shows the full title. */
.song-card .pref-title {
  font-size: 1.15rem;
  font-weight: 600;
  line-height: 1.4;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.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;
}

/* One row, wrapping where the card is narrow: with the title reserve gone
   (cards hug content) a wrapped badge row costs nothing to protect against,
   so wrapping replaces the old nowrap-and-ellipsise compensation. auto still
   pushes the badges to the bottom, so they line up across a grid row however
   tall the cards in it end up. */
.badges {
  margin-top: auto;
  padding-top: 0.5rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
}

.badge {
  font-family: var(--font-ui); /* status chrome at 0.72rem, not reading text */
  white-space: nowrap;
  /* Wrapping handles a row of badges; this handles one badge wider than the
     card (large browser font sizes), which ellipsises rather than bleeding
     past the card's border. */
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  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-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 {
  font-family: var(--font-ui);
  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 -- .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-size: clamp(1.45rem, 4vw, 1.95rem);
  font-weight: 600;
  line-height: 1.3;
}

.song-head .en {
  margin: 0.1rem 0 0;
  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. */
  font-family: var(--font-ui); /* player chrome: video titles, tabs, note */
  --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; }
}
