/* ---------- Palette: green, light + dark ----------
   Page and card are warm greige-greens, ink is a deep forest, and one
   green (--hw-accent) means "action": buttons, active tab, links, the
   Beam pill. Contrast was measured on the surface each colour sits on
   (4.5:1 text, 3:1 icons/fills). --hw-on-accent is the text colour on
   that green (white in light, near-black in dark, where the green is
   light). --hw-love is only the favourite heart, kept warm so green
   stays "action". Honey (#F2A200) is brand-only (the bee in the icon),
   never used for words: it is 2.0:1 on the light page.
   --hw-muted is decorative (rules, disabled fills), never body text. */
:root {
    --hw-bg: #EEF0E8;
    --hw-card: #F8F9F4;
    --hw-ink: #2F3D32;
    --hw-ink-soft: #686F63;
    --hw-muted: #C9D1C2;
    --hw-accent: #42654D;
    --hw-on-accent: #FFFFFF;
    --hw-love: #C0392B;
    --hw-border: #DDE3D6;
    --font-voice: 'Noto Serif Devanagari', 'Georgia', serif;
    --font-sans: 'Inter', system-ui, sans-serif;
    /* Built-in CSS easings are too weak to feel intentional (emil-design-eng).
       Used for every deliberate UI transition below. */
    --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}

/* Default WebKit tap highlight reads as a bug, not a designed interaction —
   replaced everywhere below with deliberate :active feedback instead. */
* {
    -webkit-tap-highlight-color: transparent;
}

/* Dark theme. The app starts LIGHT, whatever the phone's setting; Settings has a Light / Dark choice, kept on the device and put on <html> as
   data-theme="dark" before the first paint (base.html.twig). It deliberately does not follow the device's dark mode. */
:root { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

:root[data-theme="dark"] {
    --hw-bg: #131A15;
    --hw-card: #1B241E;
    --hw-ink: #E7EBE2;
    --hw-ink-soft: #A7B0A2;
    --hw-muted: #3A463D;
    --hw-accent: #7DB08D;
    --hw-on-accent: #0F1F16;
    --hw-love: #FF8A8F;
    --hw-border: #2A352D;
}

* { box-sizing: border-box; }

html, body {
    margin: 0;
    padding: 0;
    background: var(--hw-bg);
    color: var(--hw-ink);
    font-family: var(--font-sans);
    -webkit-text-size-adjust: 100%; /* prevents iOS auto-zoom side effects on rotate */
    /* Layer 2 of 3 for disabling zoom (see the viewport meta tag and the
       gesturestart listener in base.html.twig for the other two).
       pan-x/pan-y permits scrolling but disallows pinch-zoom AND
       double-tap-zoom. Text-size adjustment for reading is handled by the
       in-app A-/A+ controls on the lyrics page instead. */
    touch-action: pan-x pan-y;
}

/* ---------- Safe-area handling (notch / home indicator) ---------- */
.app-shell {
    padding-top: env(safe-area-inset-top);
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
    min-height: 100vh;
}

/* ---------- Buttons ----------
   IMPORTANT: `all: unset` resets `display` to its initial value (`inline`),
   which is exactly the bug that made the hymn list render as wrapping tiles
   instead of a stacked list during design review. Every interactive element
   below sets `display` explicitly alongside the reset — never rely on the
   browser default after `all: unset`. */
button {
    all: unset;
    /* `all: unset` also resets box-sizing to content-box, which silently undoes the global
       `* { box-sizing: border-box }` for every <button>. Any button with `width: 100%` plus padding then
       came out wider than its container — the Beam chooser's rows overflowed their menu by 20px and the
       group codes poked past its border. Measured in a real browser: only those two cases changed size
       when this was added; every other button in the app is unaffected. */
    box-sizing: border-box;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
}

/* ---------- Links ----------
   Plain <a> elements (the back arrow and settings gear in the header, for one) had no
   class of their own, so they fell back to the browser's default underline and blue/purple
   link color — an underline appeared under the icons. Only some link classes had
   text-decoration: none; this makes it the baseline for every link instead of fixing them
   one at a time. A bare element selector is the lowest specificity, so any class that
   sets its own color still wins. */
a {
    color: inherit;
    text-decoration: none;
}

/* Voice search can't work on this page (e.g. loaded over http). Dimmed, but still tappable —
   tapping explains why, which a disabled button could never do. */
.is-unsupported {
    opacity: 0.45;
}

/* The `hidden` attribute must always win. Without this, any element given an explicit display
   (every <button>, via the reset above) ignores `hidden` and stays visible. */
[hidden] {
    display: none !important;
}

/* ---------- Layout primitives ---------- */
.card {
    background: var(--hw-bg);
    border-radius: 20px;
    padding: 12px;
    max-width: 480px;
    margin: 0 auto;
}

.card-inner {
    background: var(--hw-card);
    border-radius: 14px;
    /* overflow: clip, NOT hidden. Both clip the rounded corners visually, but
       `hidden` turns this element into a scroll container — and
       `position: sticky` descendants (the tab bar was sticky; it is fixed now, but this stays the safe choice)
       stick relative to their nearest scroll container. Since this element
       never actually scrolls (it just grows with its content), a sticky bar
       inside it never stuck at all: it sat at the end of the content and got
       pushed off-screen every time the font size was increased, forcing a
       scroll back down to reach the A-/A+ buttons again. `clip` doesn't
       create a scroll container, so sticky resolves against the viewport
       as intended. Supported in Safari 16+, Chrome 90+, Firefox 81+. */
    overflow: clip;
    border: 0.5px solid var(--hw-border);
}

/* Plain header, no special "cover" banding — reverted per "no heavy
   theming". A hairline bottom border is enough to separate it from the
   content below on a mostly-white surface. */
.header-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 16px 20px 14px;
    border-bottom: 0.5px solid var(--hw-border);
}

.brand {
    font-family: var(--font-voice);
    letter-spacing: 0.01em;
    font-size: 19px;
    color: var(--hw-ink);
}

/* ---------- Search bar ---------- */
.search-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    background: var(--hw-bg);
    border: 0.5px solid var(--hw-border);
    border-radius: 999px;
    padding: 10px 14px;
    margin: 0 20px 12px;
}

.search-bar input {
    all: unset;
    display: block;
    flex: 1;
    /* `all: unset` above resets min-width to `auto`, so a flex input refuses to shrink below its intrinsic
       width (~170px). At 320px that pushed the microphone button 24px out of the search pill, where the
       card clips it. min-width: 0 lets the field shrink to fit. */
    min-width: 0;
    /* 16px minimum — anything smaller triggers iOS Safari's auto-zoom-on-focus,
       which is jarring on a search-first screen. */
    font-size: 16px;
    color: var(--hw-ink);
    font-family: var(--font-sans);
}

.search-bar input::placeholder {
    color: var(--hw-ink-soft);
}

.search-bar .icon-btn {
    color: var(--hw-ink-soft);
    flex-shrink: 0;
}

.search-bar .icon-btn.accent {
    color: var(--hw-accent);
}

.search-bar .icon-btn[disabled] {
    color: var(--hw-ink-soft);
    opacity: .55;
    cursor: not-allowed;
}

/* ---------- Category chips ---------- */
.chip-row {
    display: flex;
    gap: 8px;
    padding: 0 20px 14px;
    overflow-x: auto;
    border-bottom: 0.5px solid var(--hw-border);
}

.chip {
    display: inline-block;
    font-size: 13px;
    padding: 7px 16px;
    border-radius: 999px;
    border: 0.5px solid var(--hw-border);
    color: var(--hw-ink-soft);
    white-space: nowrap;
    text-decoration: none;
    transition: transform 100ms var(--ease-out), border-color 150ms var(--ease-out);
}

.chip.active {
    background: var(--hw-accent);
    color: var(--hw-on-accent);
    border-color: var(--hw-accent);
}

/* Feedback on press, not just on the tap completing (apple-design: "respond
   on pointer-down"). Untagged chips get a press scale; gated hover is for
   desktop pointers only, so a tap doesn't leave a stuck hover state. */
.chip:active {
    transform: scale(0.96);
}

@media (hover: hover) and (pointer: fine) {
    .chip:not(.active):hover {
        border-color: var(--hw-ink-soft);
    }
}

/* ---------- Hymn list rows ---------- */
.hymn-row {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    width: 100%;
    padding: 14px 20px;
    border-bottom: 0.5px solid var(--hw-border);
    text-align: left;
    text-decoration: none;
    transition: background-color 100ms var(--ease-out);
}

.hymn-row:last-child {
    border-bottom: none;
}

.hymn-row:active {
    background-color: var(--hw-border);
}

@media (hover: hover) and (pointer: fine) {
    .hymn-row:hover {
        background-color: color-mix(in srgb, var(--hw-border) 50%, transparent);
    }
}

.hymn-row .num {
    font-size: 14px;
    color: var(--hw-ink-soft);
    width: 20px;
    flex-shrink: 0;
    padding-top: 2px;
    font-variant-numeric: tabular-nums;
    /* Small text wants slightly positive tracking for legibility (apple-design) */
    letter-spacing: 0.02em;
}

.hymn-row .titles {
    flex: 1;
    min-width: 0;
}

.hymn-row .title-native {
    display: block;
    font-family: var(--font-voice);
    font-size: 18px;
    line-height: 1.4;
    color: var(--hw-ink);
    /* No negative tracking here despite this being the larger end of the
       scale (apple-design's usual guidance for display text) — Devanagari
       conjuncts and matras are positioned precisely against neighboring
       glyphs, and tightening tracking risks visible collisions that don't
       happen with Latin type. This is a deliberate exception, not an
       oversight. */
}

.hymn-row .title-sub {
    display: block;
    font-size: 13px;
    color: var(--hw-ink-soft);
    margin-top: 2px;
    font-style: italic;
}

/* Favorited row indicator — a small filled heart before the title, so the
   browse/search lists show favorite status without opening each hymn. */
.hymn-row.is-favorite .title-native::before {
    content: "♥";
    color: var(--hw-love);
    font-size: 14px;
    margin-right: 6px;
    vertical-align: middle;
}

/* "Matched by meaning" trust signal — shown when a result has no lexical/
   spelling overlap with the query and was found purely via semantic
   search, so it doesn't look like a mistake. */
.match-badge {
    display: inline-block;
    font-size: 10px;
    color: var(--hw-accent);
    border: 0.5px solid var(--hw-accent);
    border-radius: 999px;
    padding: 1px 8px;
    margin-top: 4px;
}

/* Zero-result search state — an actual next step, not a dead end. */
.empty-state {
    padding: 24px 20px;
    text-align: center;
}

.empty-state p {
    margin: 0 0 6px;
    font-size: 13px;
    color: var(--hw-ink-soft);
}

.empty-state-hint {
    font-size: 12px !important;
}

/* ---------- Lyrics page ---------- */
.lyrics-title-block {
    padding: 20px 24px 4px;
    text-align: left;
}

/* A short quiet rule under the title, in place of a full-width border. Decorative, so --hw-muted. */
.lyrics-title-block::after {
    content: '';
    display: block;
    width: 40px;
    height: 3px;
    margin-top: 20px;
    border-radius: 2px;
    background: var(--hw-muted);
}

.lyrics-meta {
    /* Tune/reference notes under the title (e.g. "R.CH.9"), only rendered when present.
       pre-line preserves line breaks for hymns with more than one note. */
    font-size: 12px;
    letter-spacing: 0.02em;
    line-height: 1.5;
    color: var(--hw-ink-soft);
    margin-top: 4px;
    white-space: pre-line;
}

.lyrics-title-native {
    font-family: var(--font-voice);
    font-size: 24px;
    line-height: 1.4;
    color: var(--hw-ink);
    margin-bottom: 4px;
    /* Same Devanagari exception as .hymn-row .title-native: no negative
       tracking on the largest text here, despite that being the usual
       move for display-scale type — tightening risks conjunct collisions. */
}

.lyrics-title-sub {
    font-size: 13px;
    color: var(--hw-ink-soft);
    font-style: italic;
}

.verse-block {
    display: flex;
    gap: 12px;
    margin-bottom: 26px;
    padding: 0 24px;
}

.verse-label {
    font-size: 13px;
    letter-spacing: 0.03em;
    font-variant-numeric: tabular-nums;
    color: var(--hw-ink-soft);
    min-width: 24px;
    flex-shrink: 0;
}

.verse-text {
    font-family: var(--font-voice);
    line-height: 1.75;
    color: var(--hw-ink);
    /* font-size set inline per-element by the preferences controller */
}

.tag-row {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    padding: 0 20px 16px;
}

.tag-pill {
    font-size: 10px;
    padding: 3px 10px;
    border-radius: 999px;
    border: 0.5px solid var(--hw-border);
    color: var(--hw-ink-soft);
}

/* ---------- Toast ----------
   "Lyrics copied" / "Link copied": a small pill fixed just above the tab bar, centred (it lives in the page, not in any footer, so it does
   not depend on a bar being there or being on screen). It is a polite live region (role="status"), so it is also spoken. */
.toast {
    position: fixed;
    left: 50%;
    bottom: calc(78px + env(safe-area-inset-bottom)); /* just above the tab bar */
    z-index: 30;
    max-width: calc(100vw - 32px);
    padding: 10px 16px;
    border-radius: 999px;
    background: var(--hw-ink);
    color: var(--hw-card);
    font-size: 14px;
    line-height: 1.3;
    text-align: center;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
    opacity: 0;
    transform: translate(-50%, 6px);
    pointer-events: none;
    transition: opacity 160ms var(--ease-out), transform 160ms var(--ease-out);
}

.toast:empty {
    display: none;
}

.toast.show {
    opacity: 1;
    transform: translate(-50%, 0);
}

@media (prefers-reduced-motion: reduce) {
    .toast {
        transform: translate(-50%, 0);
        transition: opacity 160ms var(--ease-out);
    }
}

@keyframes favorite-pulse {
    0% { transform: scale(1); }
    40% { transform: scale(1.25); }
    100% { transform: scale(1); }
}

/* ---------- Book switcher (header dropdown) ---------- */
/* No overflow:hidden on any ancestor between this and .card-inner — a
   floating menu positioned inside an overflow:hidden container gets
   clipped by it, which is exactly the bug caught during mockup review
   before this was ever built for real. */
.book-menu {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    min-width: 220px;
    z-index: 10;
    background: var(--hw-card);
    border: 0.5px solid var(--hw-border);
    border-radius: 10px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
    padding: 6px;
    opacity: 0;
    transform: translateY(-6px) scale(0.98);
    pointer-events: none;
    transition: opacity 160ms var(--ease-out), transform 160ms var(--ease-out);
}

.book-menu.open {
    opacity: 1;
    transform: translateY(0) scale(1);
    pointer-events: auto;
}

.book-menu-opt {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: 10px 14px;
    border-radius: 8px;
    color: var(--hw-ink-soft);
    font-size: 14px;
    transition: background-color 100ms var(--ease-out);
}

.book-menu-opt:active {
    background: var(--hw-border);
}

.book-menu-opt i {
    color: var(--hw-accent);
}

/* ---------- Book preference (Settings page) ---------- */
.book-pref-option {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 12px;
    border: 1px solid var(--hw-border);
    border-radius: 8px;
    color: var(--hw-ink-soft);
    font-size: 14px;
    transition: border-color 150ms var(--ease-out);
}

.book-pref-option i {
    display: none;
    color: var(--hw-accent);
}

.book-pref-option.active {
    border-color: var(--hw-accent);
    color: var(--hw-ink);
}

.book-pref-option.active i {
    display: inline;
}

/* ---------- Pagination ---------- */
.pagination {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 16px 20px;
    border-top: 0.5px solid var(--hw-border);
}

.pagination-link {
    font-size: 13px;
    color: var(--hw-accent);
    text-decoration: none;
    transition: transform 100ms var(--ease-out);
}

.pagination-link:active {
    transform: scale(0.95);
}

.pagination-link.disabled {
    color: var(--hw-ink-soft);
    opacity: .55;
    pointer-events: none;
}

.pagination-status {
    font-size: 12px;
    color: var(--hw-ink-soft);
}

/* ---------- Groups (live beaming) ---------- */
.group-section {
    padding: 16px 20px;
    border-bottom: 0.5px solid var(--hw-border);
}

.section-label {
    font-size: 12px;
    color: var(--hw-ink-soft);
    margin-bottom: 8px;
}

.section-hint {
    font-size: 12px;
    line-height: 1.5;
    color: var(--hw-ink-soft);
    margin: 0 0 10px;
}

.inline-form {
    display: flex;
    gap: 8px;
}

.text-input {
    width: 100%;
    min-width: 0;
    font-family: inherit;
    font-size: 16px; /* 16px+ stops iOS zooming into the field on focus */
    padding: 10px 12px;
    border: 0.5px solid var(--hw-border);
    border-radius: 8px;
    background: var(--hw-bg);
    color: var(--hw-ink);
}

.text-input:focus {
    outline: none;
    border-color: var(--hw-accent);
}

.inline-form .text-input {
    flex: 1;
    width: auto;
}

.code-input {
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 10px 16px;
    border: 0.5px solid var(--hw-border);
    border-radius: 8px;
    font-size: 14px;
    color: var(--hw-ink);
    transition: transform 100ms var(--ease-out), background-color 100ms var(--ease-out);
}

.btn:active {
    transform: scale(0.96);
}

.btn-primary {
    background: var(--hw-accent);
    border-color: var(--hw-accent);
    color: var(--hw-on-accent);
}

.btn-quiet {
    border-color: transparent;
    color: var(--hw-ink-soft);
}

.btn-row {
    display: flex;
    gap: 8px;
    margin-top: 10px;
}

.field-error {
    margin: 8px 0 0;
    font-size: 12px;
    color: var(--hw-accent);
}

.link-quiet {
    font-size: 12px;
    color: var(--hw-ink-soft);
    text-decoration: underline;
}

.join-code {
    font-size: 28px;
    font-weight: 600;
    letter-spacing: 0.18em;
    color: var(--hw-ink);
    margin: 2px 0 10px;
}

.now-text {
    font-family: var(--font-voice);
    font-size: 17px;
    line-height: 1.4;
    color: var(--hw-ink);
    margin-bottom: 10px;
    white-space: pre-line; /* the book name sits on its own line */
}

.live-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 20px;
    font-size: 12px;
    color: var(--hw-ink-soft);
    border-bottom: 0.5px solid var(--hw-border);
}

.live-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--hw-accent);
    animation: live-pulse 1.8s ease-in-out infinite;
}

.live-dot.offline {
    background: var(--hw-muted);
    animation: none;
}

@keyframes live-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.35; }
}

@media (prefers-reduced-motion: reduce) {
    .live-dot { animation: none; }
}

.group-row {
    display: flex;
    align-items: center;
    gap: 8px;
}

.group-section .hymn-row {
    padding-left: 0;
    padding-right: 0;
}

.group-row .hymn-row {
    flex: 1;
    min-width: 0;
    border-bottom: none;
}

.beam-action {
    margin-left: auto;
    flex-shrink: 0;
    align-self: center;
    font-size: 12px;
    color: var(--hw-accent);
}

.settings-link {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 16px 20px;
    border-bottom: 0.5px solid var(--hw-border);
}

/* The "Youth group is on #45 · Open" banner, in the data-turbo-permanent slot in base.html.twig. */
/* The slot is the fixed container, so several banners (one per group) STACK instead of piling on
   top of each other. pointer-events: none keeps the empty gaps between banners from blocking taps
   on the page underneath; each banner turns them back on for itself. */
#beam-banner-slot {
    position: fixed;
    top: calc(8px + env(safe-area-inset-top));
    left: 12px;
    right: 12px;
    max-width: 456px;
    margin: 0 auto;
    z-index: 1000;
    display: flex;
    flex-direction: column;
    gap: 8px;
    pointer-events: none;
}

.beam-banner {
    pointer-events: auto;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 10px 10px 14px;
    background: var(--hw-card);
    border: 0.5px solid var(--hw-border);
    border-left: 3px solid var(--hw-accent);
    border-radius: 10px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16);
}

.beam-banner-text {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-size: 13px;
    color: var(--hw-ink);
}

.beam-banner-text span {
    font-family: var(--font-voice);
    color: var(--hw-ink-soft);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.beam-banner-open {
    font-size: 13px;
    color: var(--hw-accent);
    padding: 6px 8px;
}

.beam-banner button {
    padding: 6px;
    color: var(--hw-ink-soft);
}

/* The Beam list: "Beam hymn 81 to...", then one row per group the device leads. It opens to the left of the pill (see .pill-beam). */
.beam-menu {
    position: absolute;
    right: 0;
    bottom: calc(100% + 8px);
    min-width: 236px;
    z-index: 5;
    padding: 8px;
    background: var(--hw-card);
    border-radius: 16px;
    box-shadow: 0 12px 36px rgba(0, 0, 0, 0.26);
}

.beam-menu-title {
    padding: 6px 10px 8px;
    font-size: 12.5px;
    font-weight: 500;
    color: var(--hw-ink-soft);
}

.beam-menu-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    width: 100%;
    min-height: 52px; /* a real touch target (it was 37px) */
    padding: 0 12px;
    border-radius: 12px;
    font-size: 15px;
    color: var(--hw-ink);
    text-align: left;
    transition: background-color 100ms var(--ease-out);
}

.beam-menu-item:focus-visible {
    outline: 2px solid var(--hw-accent);
    outline-offset: -2px;
}

.beam-menu-item:active {
    background: var(--hw-border);
}

.beam-menu-code {
    font-size: 11px;
    letter-spacing: 0.08em;
    color: var(--hw-ink-soft);
}

/* ---------- Bottom tab bar ---------- */
/* ---------- Voice search overlay ---------- */
.voice-overlay {
    padding: 24px 20px;
    min-height: 220px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
}

.waveform {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    height: 48px;
    margin-bottom: 14px;
}

.waveform .bar {
    width: 4px;
    border-radius: 2px;
    background: var(--hw-accent);
    transition: height 0.1s ease-out;
    min-height: 4px;
}

.spinner {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    border: 2px solid var(--hw-border);
    border-top-color: var(--hw-accent);
    animation: spin 0.8s linear infinite;
    margin: 0 auto 14px;
}

/* Inline variant for the search bar — no block margin, never squeezed by flex. */
.spinner-sm {
    width: 16px;
    height: 16px;
    margin: 0;
    flex-shrink: 0;
}

/* Centered spinner while a list (e.g. favorites) is loading. */
.loading-row {
    display: flex;
    justify-content: center;
    padding: 32px 0;
}

/* Turbo shows this thin bar across the top when a page navigation takes longer than
   ~500ms (its built-in delay). It defaults to blue; `html` raises specificity so this
   wins over the style Turbo injects at runtime. */
html .turbo-progress-bar {
    background: var(--hw-accent);
}

@media (prefers-reduced-motion: reduce) {
    /* A spinner is functional feedback, so removing its motion entirely would leave a
       frozen ring that looks broken. Reduced motion means gentler, not nothing:
       a slow opacity pulse instead of rotation. */
    .spinner { animation: spinner-pulse 1.4s ease-in-out infinite; }

    /* Reduced motion means gentler, not zero (apple-design) — the color/
       background feedback on press stays, since it aids comprehension;
       only the scale/pulse transforms drop out. */
    .chip:active,
    .hymn-row:active,
    .tab:active .tab-pill,
    .menu-item:active,
    .menu-step:active,
    .pager-link:active {
        transform: none;
    }

    .menu-item i.active {
        animation: none;
    }
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

@keyframes spinner-pulse {
    0%, 100% { opacity: 0.35; }
    50% { opacity: 1; }
}

.voice-hint {
    font-size: 13px;
    color: var(--hw-ink-soft);
    max-width: 260px;
    line-height: 1.5;
}

.voice-stop-btn {
    margin-top: 14px;
    font-size: 13px;
    color: var(--hw-ink-soft);
    border: 0.5px solid var(--hw-border);
    border-radius: 999px;
    padding: 6px 16px;
}


/* ---------- Install card ---------- */
.install-card {
    padding: 12px 20px;
    border-bottom: 0.5px solid var(--hw-border);
    background: var(--hw-card);
}

.install-row {
    padding: 16px 20px;
}

.install-main {
    display: flex;
    align-items: center;
    gap: 12px;
}

.install-icon {
    flex-shrink: 0;
    font-size: 22px;
    color: var(--hw-accent);
}

.install-text {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-size: 13px;
    color: var(--hw-ink);
}

.install-text span {
    font-size: 12px;
    color: var(--hw-ink-soft);
}

.install-dismiss {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 44px;
    height: 44px;
    margin: -8px -12px -8px 0;
    color: var(--hw-ink-soft);
}

.install-steps {
    margin: 10px 0 2px;
    padding-left: 20px;
    font-size: 13px;
    line-height: 1.8;
    color: var(--hw-ink);
}

.install-steps i {
    vertical-align: middle;
    color: var(--hw-accent);
}


/* ---------- Tablet and larger ----------
   The phone layout is a 480px column. On an iPad, a laptop, or a TV that leaves most of the screen empty
   and the text small. Widen in steps rather than stretching: very long lines of lyrics are hard to read. */
@media (min-width: 700px) {
    .card { max-width: 720px; }
    .lyrics-title-block { padding-left: 32px; padding-right: 32px; }
    .lyrics-title-native { font-size: 24px; }
    .verse-block { padding-left: 32px; padding-right: 32px; }
}

@media (min-width: 1100px) {
    .card { max-width: 960px; }
}

/* Two columns for hymn lists once there's room, so a tablet shows twice as many hymns per screen. */
@media (min-width: 900px) {
    [data-search-target="results"] {
        display: grid;
        grid-template-columns: 1fr 1fr;
    }
    [data-search-target="results"] > .hymn-row:nth-child(odd) {
        border-right: 0.5px solid var(--hw-border);
    }
    [data-search-target="results"] .pagination {
        grid-column: 1 / -1;
    }
}


/* ---------- Presentation mode ----------
   For a projector, TV, or a tablet on a stand. Toggled by present_controller.js (button, the P key, or
   /g/CODE?present=1). Text is sized from the viewport HEIGHT: projectors are 16:9 and the constraint is
   how many lines fit on screen, not how wide it is. Wide screens then get columns automatically, so a
   long hymn shows more at once instead of needing a scroll. */
body.present {
    --hw-bg: #0B1220;
    --hw-card: #0B1220;
    --hw-ink: #F8FAFC;
    --hw-ink-soft: #CBD5E1;
    --hw-border: transparent;
    --present-text: clamp(24px, 3.6vh, 56px);
    background: var(--hw-bg);
}

body.present .header-row,
body.present .tab-bar,
body.present .tab-bar-spacer,
body.present .hymn-pager,
body.present .lyrics-number,
body.present .hymn-pill,
body.present .pinch-hint,
body.present .size-readout,
body.present .live-row,
body.present .lyrics-meta,
body.present .install-card,
body.present .leave-group,
body.present .empty-state-hint,
body.present #beam-banner-slot {
    display: none !important;
}

body.present .app-shell { padding: 0; }
body.present .card { max-width: none; padding: 0; border-radius: 0; background: var(--hw-bg); }
body.present .card-inner { border: none; border-radius: 0; min-height: 100vh; }

body.present .lyrics-title-block::after {
    margin-left: auto;
    margin-right: auto;
}

body.present .lyrics-title-block {
    border: none;
    padding: 2.5vh 6vw 0;
    text-align: center;
}

/* !important because the preferences controller sets the user's own font size inline on each verse. */
body.present .lyrics-title-native {
    font-size: calc(var(--present-text) * 1.35) !important;
    line-height: 1.3;
}

body.present [data-preferences-target="versesContainer"] {
    padding: 1.5vh 6vw 2vh !important;
    width: fit-content; /* one column, centred on the screen */
    max-width: 100%;
    margin: 0 auto;
    column-width: calc(var(--present-text) * 20);
    column-gap: 5vw;
}

body.present .verse-block {
    padding: 0;
    margin-bottom: 0.55em; /* em, not vh: spacing must shrink with the text or a long hymn never fits one column */
    gap: 1.2em;
    break-inside: avoid;
}

body.present .verse-text {
    font-size: var(--present-text) !important;
    line-height: 1.4;
}

body.present .verse-label {
    font-size: calc(var(--present-text) * 0.45);
    min-width: 4em;
    padding-top: 0.6em;
}

/* The chorus label is the short "Ch." everywhere (the markup still carries the full word in .ch-long and the abbr title). */
.verse-label .ch-long {
    display: none;
}

.verse-label .ch-short {
    display: inline;
    text-decoration: none;
}

body.present .empty-state {
    font-size: var(--present-text);
    padding-top: 30vh;
}

.present-exit {
    position: fixed;
    top: calc(12px + env(safe-area-inset-top));
    right: 12px;
    z-index: 2000;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.14);
    color: #FFFFFF;
    opacity: 0.45;
    transition: opacity 150ms var(--ease-out);
}

.present-exit:hover,
.present-exit:focus-visible {
    opacity: 1;
}


/* ---------- Beaming animation ----------
   States are set by beam_here_controller.js (data-beam-state on the button, data-state on a chooser row);
   everything visual lives here. Sending = rings radiating out (the broadcast metaphor), success = one
   bigger burst plus a pop on the check, failure = a shake and NO rings, so a failed beam can't be
   mistaken for a sent one. */
button[data-beam-state] {
    position: relative;
}

button[data-beam-state]::before,
button[data-beam-state]::after {
    content: '';
    position: absolute;
    inset: -4px;
    border: 2px solid var(--hw-accent);
    border-radius: 50%;
    opacity: 0;
    pointer-events: none;
}

button[data-beam-state="sending"]::before { animation: beam-ring 1.1s var(--ease-out) infinite; }
button[data-beam-state="sending"]::after  { animation: beam-ring 1.1s var(--ease-out) 0.35s infinite; }

button[data-beam-state="sent"]::before { animation: beam-burst 0.7s var(--ease-out) 1; }
button[data-beam-state="sent"]::after  { animation: beam-burst 0.7s var(--ease-out) 0.12s 1; }
button[data-beam-state="sent"] i       { animation: beam-pop 0.4s var(--ease-out); }

button[data-beam-state="failed"] { animation: beam-shake 0.4s var(--ease-out); }

@keyframes beam-ring {
    0%   { transform: scale(0.6); opacity: 0.8; }
    100% { transform: scale(2.1); opacity: 0; }
}

@keyframes beam-burst {
    0%   { transform: scale(0.7); opacity: 1; }
    100% { transform: scale(2.6); opacity: 0; }
}

@keyframes beam-pop {
    0%   { transform: scale(0.6); }
    60%  { transform: scale(1.25); }
    100% { transform: scale(1); }
}

@keyframes beam-shake {
    0%, 100% { transform: translateX(0); }
    20%      { transform: translateX(-4px); }
    40%      { transform: translateX(4px); }
    60%      { transform: translateX(-3px); }
    80%      { transform: translateX(3px); }
}

/* The chooser itself, and the row that was tapped. */
.beam-menu {
    transform-origin: bottom right;
    animation: beam-menu-in 140ms var(--ease-out);
}

@keyframes beam-menu-in {
    from { opacity: 0; transform: translateY(6px) scale(0.96); }
    to   { opacity: 1; transform: none; }
}

.beam-menu-item {
    position: relative;
    overflow: hidden;
}

/* A soft light sweeping across the row while the request is in flight. */
.beam-menu-item[data-state="sending"]::after {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--hw-accent) 22%, transparent), transparent);
    transform: translateX(-100%);
    animation: beam-sweep 0.9s linear infinite;
    pointer-events: none;
}

.beam-menu-item[data-state="sent"]   { animation: beam-flash 0.9s var(--ease-out); }
.beam-menu-item[data-state="failed"] { animation: beam-shake 0.4s var(--ease-out); }

/* Status text replaces the group's code on the right; it reads better without code-style spacing. */
.beam-menu-item[data-state] .beam-menu-code {
    letter-spacing: 0;
    font-size: 12px;
    color: var(--hw-accent);
    text-align: right;
}

@keyframes beam-sweep {
    to { transform: translateX(100%); }
}

@keyframes beam-flash {
    0%   { background: color-mix(in srgb, var(--hw-accent) 22%, transparent); }
    100% { background: transparent; }
}

/* The hymn that was just beamed, on the leader's search results. */
.hymn-row.beam-flash {
    animation: beam-flash 0.9s var(--ease-out);
}

/* A new hymn arriving on a member's screen (and on the projector). */
.beam-arrive {
    animation: beam-arrive 0.5s var(--ease-out);
}

@keyframes beam-arrive {
    from { opacity: 0; transform: translateY(12px); }
    to   { opacity: 1; transform: none; }
}

/* Reduced motion: no movement at all. The text and icon changes still say what happened, and a still ring
   stays visible while the state lasts so there is a cue that isn't motion. */
@media (prefers-reduced-motion: reduce) {
    button[data-beam-state]::before,
    button[data-beam-state]::after,
    button[data-beam-state] i,
    button[data-beam-state="failed"],
    .beam-menu,
    .beam-menu-item,
    .beam-menu-item::after,
    .hymn-row.beam-flash,
    .beam-arrive {
        animation: none !important;
    }
    button[data-beam-state="sending"]::before,
    button[data-beam-state="sent"]::before {
        opacity: 0.7;
        transform: scale(1.5);
    }
}


/* ---------- Beam chooser: keep content inside the menu ----------
   Beyond the box-sizing fix above: a long group name used to be able to push the code out of the row, and
   on a narrow phone the menu could be wider than the bar it opens from. */
.beam-menu {
    max-width: calc(100% - 24px);
}

.beam-menu-item > span:first-child {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.beam-menu-code {
    flex-shrink: 0;
}

/* Progress/error text ("Leader key no longer valid") may wrap instead of squeezing the group name out. */
.beam-menu-item[data-state] .beam-menu-code {
    max-width: 58%;
    white-space: normal;
}




/* ---------- Header icons (44px-ish tap targets without making the header taller) ---------- */
.header-side {
    display: flex;
    align-items: center;
    /* Equal shares of the row, so the centred title stays centred whenever there is room; when the right side's three icons need more
       than half (a 300px phone), it takes what it needs and the title shifts a little instead of anything overflowing. */
    flex: 1 1 0;
}

.header-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    margin: -12px 0; /* the header-row's own padding already provides the height */
}


/* ---------- Quick search: search from a hymn page ----------
   A full-screen sheet; the content inside reuses .card/.card-inner, so tablet widths and the rounded look
   come for free. Scroll lock is on <html> so the hymn behind can't move while the keyboard is up. */
html.quick-search-open {
    overflow: hidden;
}

.quick-search {
    position: fixed;
    inset: 0;
    z-index: 1500;
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--hw-bg);
    /* No top padding here: a pinned (sticky) element sticks to the very top of the screen and ignores its
       scroll container's padding, which on a notched iPhone would park the title and close button UNDER the
       notch. The safe-area inset is the pinned bar's own top padding instead (below). */
    padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
    animation: quick-search-in 160ms var(--ease-out);
}

@keyframes quick-search-in {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: none; }
}

body.present .quick-search {
    display: none !important;
}

@media (prefers-reduced-motion: reduce) {
    .quick-search { animation: none; }
}

/* Pinned title + search box inside the sheet. Padding, not a bottom margin: a margin would collapse out of the
   wrapper, and results scrolling underneath would show through that gap. */
.quick-search-top {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--hw-card);
    padding-top: env(safe-area-inset-top); /* its own background also covers the notch area, so results never show through it */
    padding-bottom: 12px;
}

/* ---------- Book chooser (the app's front door: /open, /books, the Search shortcut) ---------- */
.chooser-inner {
    padding: 32px 20px 24px;
}

.chooser-head {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    margin-bottom: 24px;
    text-align: center;
}

.chooser-mark {
    width: 56px;
    height: 56px;
    margin-bottom: 12px;
    border-radius: 14px;
}

.chooser-title {
    font-size: 20px;
    font-weight: 600;
    letter-spacing: -0.01em;
    color: var(--hw-ink);
}

.chooser-sub {
    font-size: 13px;
    color: var(--hw-ink-soft);
}

.chooser-list {
    display: grid;
    gap: 10px;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* A short stagger on arrival; the startup splash is still fading out over this on a cold launch, so it reads as one motion. */
.chooser-list li {
    animation: chooser-in 320ms var(--ease-out) both;
    animation-delay: min(calc(var(--i, 0) * 50ms), 400ms);
}

@keyframes chooser-in {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: none; }
}

.chooser-book {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: 64px;
    padding: 14px 16px 14px 18px;
    border: 1px solid var(--hw-border);
    border-radius: 14px;
    background: var(--hw-card);
    color: var(--hw-ink);
    text-decoration: none;
    transition: transform 120ms var(--ease-out), border-color 150ms var(--ease-out), background-color 150ms var(--ease-out);
}

.chooser-book:active {
    transform: scale(0.985);
    background: var(--hw-bg);
}

.chooser-book:focus-visible {
    outline: 2px solid var(--hw-accent);
    outline-offset: 2px;
}

@media (hover: hover) {
    .chooser-book:hover {
        background: var(--hw-bg);
    }
}

/* The name and, when it applies, its "Last opened" badge. Side by side normally; stacked on the narrowest phones, where a badge beside a
   long title squeezes the title into a sliver (seven lines at 320px). */
.chooser-text {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 12px;
}

@media (max-width: 359px) {
    .chooser-text {
        flex-direction: column;
        align-items: flex-start;
        gap: 8px;
    }
}

.chooser-name {
    flex: 1;
    min-width: 0;
    font-family: var(--font-voice);
    font-size: 18px;
    line-height: 1.35;
    overflow-wrap: anywhere;
}

.chooser-book > i {
    flex: none;
    color: var(--hw-ink-soft);
}

.chooser-book.is-last {
    border-color: var(--hw-accent);
}

.chooser-badge {
    flex: none;
    padding: 4px 9px;
    border-radius: 999px;
    background: var(--hw-accent);
    color: var(--hw-on-accent);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
    white-space: nowrap;
}

.chooser-empty {
    padding: 12px 4px;
    font-size: 14px;
    color: var(--hw-ink-soft);
    text-align: center;
}

@media (prefers-reduced-motion: reduce) {
    .chooser-list li { animation: none; }
    .chooser-book { transition: none; }
}

/* ---------- Presentation: one verse at a time ("slides") ----------
   present_controller.js adds `present-slides` (on top of `present`) and present-deck.js marks ONE of the page's own blocks `.is-current`:
   the title block, or a .verse-block. Everything else is hidden. The text size is set by the deck (--slide-font) to the largest that
   fits, so a four-line verse is huge and a long one is smaller but still whole. The whole-hymn view is the `present` rules above, untouched. */
/* On the page it is the small "HYMN 81" eyebrow above the title; on the projector's title slide the rules below restyle it. */
.lyrics-number {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 10px;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--hw-accent);
}

.lyrics-number:empty { display: none; }

.lyrics-number::after {
    content: '';
    width: 23px;
    height: 2px;
    background: var(--hw-accent);
}

body.present-slides .card-inner {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    min-height: 100dvh;
}

body.present-slides [data-preferences-target="versesContainer"] {
    display: contents; /* the verse blocks become flex children of the card, so the current one is centred on the screen */
}

body.present-slides .tag-row,
body.present-slides .lyrics-title-block:not(.is-current),
body.present-slides .verse-block:not(.is-current) {
    display: none;
}

body.present-slides .is-current {
    width: min(88vw, 1500px);
    max-height: 72vh; /* leaves 14% above and below: room for the key hint at the top and the dots at the bottom */
    max-height: 72dvh;
    margin: 0;
    padding: 0;
    overflow: hidden; /* so the deck can tell when text does not fit; it falls back to scrolling for an enormous verse */
    text-align: center;
    animation: slide-in 180ms var(--ease-out);
}

@keyframes slide-in {
    from { opacity: 0; transform: translateY(1.2vh); }
    to { opacity: 1; transform: none; }
}

body.present-slides .verse-block.is-current {
    display: block;
}

body.present-slides .verse-label {
    display: block;
    min-width: 0;
    margin: 0 0 2.4vh;
    padding: 0;
    font-size: clamp(14px, 2.3vh, 28px);
    letter-spacing: 0.16em;
    text-transform: uppercase;
}

body.present-slides .verse-text {
    font-size: var(--slide-font, 5vh) !important; /* !important: the preferences controller sets the reader's own size inline */
    line-height: 1.38;
}

/* Devanagari fonts have glyph boxes taller than a 1.38 line, so the last line's box pokes out of its block by ~0.09em. The browser counts
   that as overflow (the deck's "does it fit?" test then failed at every size and shrank every verse to the minimum) and the slide's
   overflow:hidden would clip real descenders. Padding makes room for it. overflow-wrap: a single very long word wraps, not runs off. */
body.present-slides .verse-text,
body.present-slides .lyrics-title-block .lyrics-title-native {
    padding-block: 0.15em;
    overflow-wrap: anywhere;
}

body.present-slides .lyrics-number::after { display: none; }

body.present-slides .lyrics-number {
    display: block;
    margin: 0 0 3vh;
    font-size: clamp(16px, 3.2vh, 38px);
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--hw-ink-soft);
}

body.present-slides .lyrics-title-block .lyrics-title-native {
    font-size: var(--slide-font, 8vh) !important;
    line-height: 1.22;
}

/* B: a black screen (between songs, during a prayer). The exit button hides too, or it would glow on the projector. */
body.present-blank,
body.present-blank .card,
body.present-blank .card-inner {
    background: #000;
}

body.present-blank .card-inner,
body.present-blank .present-ui,
body.present-blank .present-exit {
    visibility: hidden;
}

.present-ui {
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(14px + env(safe-area-inset-bottom));
    z-index: 1900;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    pointer-events: none;
    transition: opacity 250ms var(--ease-out);
}

.present-ui[hidden] {
    display: none;
}

.present-progress {
    display: flex;
    gap: 2px;
    pointer-events: auto;
}

body:not(.present-slides) .present-progress {
    display: none;
}

.present-dot {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
}

.present-dot::before {
    content: '';
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.35);
    transition: transform 150ms var(--ease-out), background-color 150ms var(--ease-out);
}

.present-dot[aria-current="step"]::before {
    background: #FFFFFF;
    transform: scale(1.4);
}

.present-dot:focus-visible::before {
    outline: 2px solid var(--hw-accent);
    outline-offset: 3px;
}

/* At the TOP, centred: it used to share a box with the dots, which pushed the dots up into the bottom of a tall slide. */
.present-hint {
    position: fixed;
    top: calc(14px + env(safe-area-inset-top));
    left: 50%;
    transform: translateX(-50%);
    width: max-content;
    max-width: min(90vw, 640px);
    padding: 6px 14px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.12);
    color: #E2E8F0;
    font-size: 14px;
    text-align: center;
    opacity: 0;
    transition: opacity 300ms var(--ease-out);
}

.present-hint.show {
    opacity: 1;
}

/* After a few idle seconds the dots, hint and exit button fade out and the pointer hides: nothing over the lyrics. */
body.present-idle {
    cursor: none;
}

body.present-idle .present-ui,
body.present-idle .present-exit {
    opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
    body.present-slides .is-current { animation: none; }
    .present-dot::before,
    .present-hint,
    .present-ui { transition: none; }
}

/* ---------- QR code to join a group ----------
   Always black on white, in light AND dark mode: scanners expect dark modules on a light ground, and an inverted code is the classic way
   to make one that many phones cannot read. The white tile (quiet zone included) is drawn by the SVG itself, not by a theme colour. */
.qr {
    width: 100%;
    max-width: 220px;
    border: 1px solid var(--hw-border);
    border-radius: 12px;
    overflow: hidden;
}

.qr svg {
    display: block;
    width: 100%;
    height: auto;
}

.qr-panel {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
    margin: 14px 0;
}

.qr-dialog {
    width: min(92vw, 460px);
    padding: 24px 24px 20px;
    border: none;
    border-radius: 20px;
    background: var(--hw-card);
    color: var(--hw-ink);
    text-align: center;
}

.qr-dialog::backdrop {
    background: rgba(0, 0, 0, 0.65);
}

.qr-dialog[open] {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
}

.qr-big {
    width: min(76vw, 52vh, 380px);
}

.qr-big svg {
    display: block;
    width: 100%;
    height: auto;
    border-radius: 12px;
}

.qr-dialog-code {
    font-size: 30px;
    font-weight: 600;
    letter-spacing: 0.18em;
}

/* On the projector only: the waiting screen invites the room to scan. */
.waiting-qr {
    display: none;
}

body.present .empty-state--join {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2.5vh;
    padding-top: 6vh;
    text-align: center;
}

body.present .waiting-qr {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2vh;
}

body.present .waiting-qr .qr {
    width: min(38vh, 38vw);
    max-width: none;
}

body.present .waiting-qr-text {
    font-size: calc(var(--present-text) * 0.8);
    color: var(--hw-ink-soft);
}

body.present .waiting-qr-text strong {
    color: var(--hw-ink);
    letter-spacing: 0.14em;
}

/* ---------- "Notify me when a hymn is beamed" (push_controller.js) ---------- */
.push-row {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 4px 20px 12px;
}

.push-row[hidden] {
    display: none;
}

.push-switch {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: 44px;
    width: 100%;
    text-align: left;
    color: var(--hw-ink);
}

.push-label {
    font-size: 14px;
}

.push-track {
    position: relative;
    flex: none;
    width: 42px;
    height: 24px;
    border-radius: 999px;
    background: var(--hw-muted);
    transition: background-color 160ms var(--ease-out);
}

.push-thumb {
    position: absolute;
    top: 2px;
    left: 2px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: #FFFFFF;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
    transition: transform 160ms var(--ease-out);
}

.push-switch[aria-checked="true"] .push-track {
    background: var(--hw-accent);
}

.push-switch[aria-checked="true"] .push-thumb {
    transform: translateX(18px);
}

.push-switch:focus-visible .push-track {
    outline: 2px solid var(--hw-accent);
    outline-offset: 3px;
}

.push-switch:disabled {
    opacity: 0.6;
}

.push-note {
    font-size: 13px;
    line-height: 1.45;
    color: var(--hw-ink-soft);
}

.push-note[hidden] {
    display: none;
}

/* The projector is for the room, not for anyone's phone settings. */
body.present .push-row {
    display: none !important;
}

@media (prefers-reduced-motion: reduce) {
    .push-track,
    .push-thumb {
        transition: none;
    }
}

/* The card's max-width at each breakpoint (it is set on .card above), shared so the fixed tab bar can be exactly as wide as the card. */
:root { --card-max: 480px; }
@media (min-width: 700px) { :root { --card-max: 720px; } }
@media (min-width: 1100px) { :root { --card-max: 960px; } }

/* Room at the end of every page, so the last row can scroll clear of the fixed tab bar (its height plus the home-indicator inset). */
.tab-bar-spacer {
    height: calc(70px + env(safe-area-inset-bottom));
}

/* ---------- Navigation: the tab bar (templates/_tab_bar.html.twig) ----------
   The same four tabs on every page. The current one is marked by a tinted pill behind its icon and a bolder label, so where you are
   is never a guess. On the lyrics page it slides away while you scroll down to read (reading_bar_controller.js). */
.tab-bar {
    display: flex;
    padding: 6px 6px calc(8px + env(safe-area-inset-bottom)); /* clear of the home indicator on notched iPhones */
    border-top: 0.5px solid var(--hw-border);
    /* Pinned to the bottom of the SCREEN, not of the page: on a short page (an empty Favorites, the waiting group page) a sticky bar would
       sit in mid-screen just under the content, and navigation that moves with the amount of content is not constant. It is as wide as the
       card (the card's own max-width: see --card-max), centred. */
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 5;
    max-width: var(--card-max);
    margin: 0 auto;
    background: var(--hw-card);
    transition: transform 200ms var(--ease-out);
}

.tab-bar.is-hidden {
    transform: translateY(100%);
}

.tab {
    display: flex;
    flex: 1;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    min-height: 52px;
    padding-top: 2px;
    color: var(--hw-ink-soft);
    font-size: 11px;
    text-decoration: none;
}

.tab-pill {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 58px;
    height: 30px;
    border-radius: 15px;
    font-size: 20px;
    transition: background-color 160ms var(--ease-out), transform 100ms var(--ease-out);
}

.tab:active .tab-pill {
    transform: scale(0.92);
}

.tab:focus-visible {
    outline: 2px solid var(--hw-accent);
    outline-offset: -2px;
    border-radius: 12px;
}

.tab.is-active {
    color: var(--hw-ink);
    font-weight: 600;
}

.tab.is-active .tab-pill {
    background: #CCD5CC; /* fallback for browsers without color-mix */
    background: color-mix(in srgb, var(--hw-accent) 24%, transparent);
}

/* ---------- Hymn page header: heart, text size, "..." ---------- */
.header-actions {
    display: flex;
    align-items: center;
}

/* The JS toggles `.active` on the icon itself (favoriteIconTarget). Animated rather than an instant snap, with a brief pulse on the
   moment it is favourited: a bit of delight on a rare, positive action, not on every tap. */
/* A group's live page has no hymn until the leader beams one: the heart and "..." wait for preferences to set data-has-hymn. */
[data-controller~="preferences"]:not([data-has-hymn]) .hymn-only {
    display: none;
}

.hymn-menu {
    position: relative;
}

.hymn-menu-trigger i {
    color: var(--hw-ink-soft);
}

.hymn-menu-scrim {
    position: fixed;
    inset: 0;
    z-index: 19; /* above the page and the tab bar, below the panel */
}

.hymn-menu-scrim[hidden] {
    display: none;
}

.hymn-menu-panel {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    z-index: 20;
    width: min(256px, calc(100vw - 24px));
    padding: 6px;
    border-radius: 16px;
    background: var(--hw-card);
    box-shadow: 0 12px 36px rgba(0, 0, 0, 0.26);
    transform-origin: top right;
    animation: menu-in 160ms var(--ease-out);
}

.hymn-menu-panel[hidden] {
    display: none;
}

@keyframes menu-in {
    from { opacity: 0; transform: translateY(-6px) scale(0.96); }
    to { opacity: 1; transform: none; }
}

.menu-item {
    position: relative;
    display: flex;
    align-items: center;
    gap: 14px;
    width: 100%;
    min-height: 48px;
    padding: 0 12px;
    border-radius: 10px;
    color: var(--hw-ink);
    font-size: 15px;
    text-align: left;
    white-space: nowrap;
    transition: background-color 100ms var(--ease-out), transform 100ms var(--ease-out);
}

.menu-item i {
    flex: none;
    width: 22px;
    font-size: 20px;
    text-align: center;
    color: var(--hw-ink-soft);
}

.menu-item:active {
    background: var(--hw-bg);
    transform: scale(0.98);
}

@media (hover: hover) {
    .menu-item:hover {
        background: var(--hw-bg);
    }
}

.menu-item:focus-visible {
    outline: 2px solid var(--hw-accent);
    outline-offset: -2px;
}

/* The beam animation draws rings round the BUTTON; in a full-width row that would be a stretched oval, so draw them round the icon. */
/* ---------- On to the next hymn (end of the lyrics) ---------- */
.hymn-pager {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin: 8px 20px 4px;
    padding-top: 6px;
    border-top: 0.5px solid var(--hw-border);
}

.pager-link {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 52px;
    margin: 0 -10px;
    padding: 0 10px;
    border-radius: 10px;
    color: var(--hw-ink-soft);
    text-decoration: none;
    transition: background-color 100ms var(--ease-out);
}

.pager-link .pg {
    display: flex;
    flex-direction: column;
    gap: 2px;
    line-height: 1.2;
}

.pager-link[rel="next"] .pg { text-align: right; }

.pager-link small {
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--hw-ink-soft);
}

.pager-link b {
    font-size: 16px;
    font-weight: 500;
    color: var(--hw-accent);
}

.pager-link i {
    font-size: 20px;
    color: var(--hw-accent);
}

.pager-flip {
    transform: rotate(180deg);
}

.pager-link:active {
    background: var(--hw-bg);
}

@media (prefers-reduced-motion: reduce) {
    .tab-bar,
    .tab-pill { transition: none; }
    .hymn-menu-panel { animation: none; }
    .menu-item { transition: none; }
}

/* ---------- The "..." menu's own rows: Favorite, and text size A- / A+ ---------- */
.menu-item i.active {
    color: var(--hw-love);
    animation: favorite-pulse 250ms var(--ease-out);
}

/* The text size row is not a button itself (it holds two): same height and padding as the others. */
.menu-size {
    gap: 8px;
    padding-right: 8px;
}

.menu-size-label {
    flex: 1;
}

.menu-step {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 36px;
    border-radius: 10px;
    background: var(--hw-bg);
    color: var(--hw-ink);
    font-weight: 600;
    transition: transform 100ms var(--ease-out);
}

.menu-step:active {
    transform: scale(0.92);
}

.menu-step:focus-visible {
    outline: 2px solid var(--hw-accent);
    outline-offset: 1px;
}

.menu-size-readout {
    min-width: 36px;
    font-size: 13px;
    text-align: center;
    color: var(--hw-ink-soft);
}

.menu-sep {
    height: 0.5px;
    margin: 4px 8px;
    background: var(--hw-border);
}

/* ---------- The floating pill: Favorite, Copy, Share ----------
   A small vertical strip on the right edge, about two-thirds of the way down, where the right thumb rests. (Beam, for a group leader, is the
   first row of the "..." menu.) It slides off to the right with the tab bar while you read, and is not on the projector. The shadow is
   deliberately faint: a hairline and a soft glow, not a lifted card. */
.hymn-pill {
    position: fixed;
    /* 8px inside the card's right edge: on a phone that is the screen edge, on a tablet or desktop it is the CARD's edge. */
    right: max(8px, calc((100vw - var(--card-max)) / 2 + 8px));
    top: 62%;
    z-index: 6;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 2px;
    border-radius: 22px;
    background: var(--hw-card);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.10), 0 0 0 0.5px rgba(0, 0, 0, 0.06);
    transform: translateY(-50%);
    transition: transform 200ms var(--ease-out);
}

/* In dark mode the card colour is the page colour, so the pill needs a surface of its own, and a hairline, or it disappears. */
:root[data-theme="dark"] .hymn-pill {
    background: color-mix(in srgb, var(--hw-ink) 10%, var(--hw-card));
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35), 0 0 0 1px color-mix(in srgb, var(--hw-ink) 16%, transparent);
}

html.bar-hidden .hymn-pill {
    transform: translate(calc(100% + 16px), -50%);
}

.pill-btn {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 40px;
    border-radius: 18px;
    color: color-mix(in srgb, var(--hw-ink) 80%, transparent);
    font-size: 17px;
    transition: background-color 100ms var(--ease-out), transform 100ms var(--ease-out);
}

/* The tap target is wider than the button looks: 44px across. */
.pill-btn::before {
    content: '';
    position: absolute;
    inset: 0 -4px;
}

.pill-btn:active {
    background: var(--hw-border);
    transform: scale(0.92);
}

.pill-btn:focus-visible {
    outline: 3px solid var(--hw-ink);
    outline-offset: 1px;
}

/* A favourited hymn: a solid red heart (a mask, so no extra glyph is needed in the icon font), with a small pop. */
.pill-fav i.active {
    color: var(--hw-love);
    animation: favorite-pulse 250ms var(--ease-out);
}

.pill-fav i.active::before {
    content: '';
    display: block;
    width: 1em;
    height: 1em;
    background: currentColor;
    -webkit-mask: var(--heart-solid) center / contain no-repeat;
    mask: var(--heart-solid) center / contain no-repeat;
}

.pill-fav i.active {
    --heart-solid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6.979 3.074a6 6 0 0 1 4.988 1.425l.037.033.034-.03a6 6 0 0 1 4.733-1.44l.246.036a6 6 0 0 1 3.364 10.008l-.18.185-.048.041-7.45 7.379a1 1 0 0 1-1.313.082l-.094-.082-7.493-7.422A6 6 0 0 1 6.979 3.074z'/%3E%3C/svg%3E");
}

/* Keep the lyrics clear of the pill. (Not in presentation mode: the pill is not there.) */
html.has-pill body:not(.present) .verse-block {
    padding-right: 52px;
}

/* The pill's Beam button (a group leader only). The group list opens to the LEFT of the pill, growing upward from the button. */
.pill-beam {
    position: relative;
}

.pill-beam .pill-btn i {
    color: var(--hw-accent);
}

.pill-beam .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

.pill-beam [data-state="sent"] { animation: beam-flash 0.9s var(--ease-out); }
.pill-beam [data-state="failed"] { animation: beam-shake 0.4s var(--ease-out); }

.pill-beam .beam-menu {
    right: calc(100% + 10px);
    bottom: 0;
    transform-origin: bottom right;
}

/* ---------- Pinch to resize: the hint (once) and the readout (while pinching) ---------- */
.pinch-hint,
.size-readout {
    position: fixed;
    left: 50%;
    z-index: 30;
    padding: 11px 16px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--hw-ink) 94%, transparent);
    color: var(--hw-bg);
    font-size: 13px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.28);
    pointer-events: none; /* decorative: it must never swallow a tap meant for what is under it */
}

.pinch-hint {
    bottom: calc(80px + env(safe-area-inset-bottom));
    display: flex;
    align-items: center;
    gap: 10px;
    max-width: calc(100vw - 32px);
    white-space: nowrap;
    transform: translateX(-50%);
    animation: hint-in 200ms var(--ease-out);
}

@keyframes hint-in {
    from { opacity: 0; transform: translateX(-50%) translateY(6px); }
    to { opacity: 1; transform: translateX(-50%); }
}

.size-readout {
    top: 96px;
    padding: 9px 18px;
    font-size: 14px;
    font-weight: 600;
    opacity: 0;
    pointer-events: none;
    transform: translateX(-50%);
    transition: opacity 150ms var(--ease-out);
}

.size-readout.show {
    opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
    .hymn-pill,
    .pill-btn,
    .menu-step,
    .size-readout { transition: none; }
    .pinch-hint { animation: none; }
}


/* ---------- Settings: Appearance (Auto / Light / Dark) ---------- */
.theme-choice {
    display: flex;
    gap: 8px;
}

.theme-option {
    flex: 1;
    min-height: 44px;
    padding: 0 12px;
    border: 0.5px solid var(--hw-border);
    border-radius: 10px;
    background: transparent;
    color: var(--hw-ink);
    font: inherit;
    font-size: 14px;
    cursor: pointer;
}

.theme-option[aria-pressed="true"] {
    background: var(--hw-accent);
    border-color: var(--hw-accent);
    color: var(--hw-on-accent);
}


/* ---------- Favorites: remove a hymn from the list ---------- */
.fav-item {
    display: flex;
    align-items: center;
    border-bottom: 0.5px solid var(--hw-border);
}

.fav-item:last-child {
    border-bottom: none;
}

.fav-item .hymn-row {
    flex: 1;
    min-width: 0;
    border-bottom: none;
}

.fav-remove {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 48px;
    height: 48px;
    margin-right: 8px;
    border-radius: 50%;
    color: var(--hw-ink-soft);
    font-size: 18px;
    transition: background-color 100ms var(--ease-out);
}

.fav-remove:active {
    background: var(--hw-border);
}

.fav-item.is-removed {
    justify-content: space-between;
    min-height: 56px;
    padding: 0 12px 0 20px;
    font-size: 13px;
    color: var(--hw-ink-soft);
}

.fav-undo {
    min-height: 44px;
    padding: 0 12px;
    color: var(--hw-accent);
    font-size: 14px;
    font-weight: 500;
}
