/* Lumbre listener page: the one layout every palette shares. It reads colors and type only from the theme tokens
   (tokens.css); nothing here names a color.
   States, on <html>: data-state connecting | waiting | live | ended; data-link down while the service's socket is
   lost; data-translator connecting | live | reconnecting | stopped | failed, the live service's translator as the
   service reports it (absent with no word of one); data-pause true while the English is on its way;
   data-listening off | ready | loading | playing; data-browsing while reading back; data-size the text size.
   Nothing here is position: fixed. The page itself never scrolls, so nothing needs it, and Safari takes a fixed box on a
   screen edge for a toolbar of the page's own: it holds the tint of its bars at the color it first found there for the
   life of the tab, whatever the page paints after. Laid out absolutely, its bars show the root's background, live. */

:root {
  --scale: 1;
  --caption-base: clamp(28px, 7.9vw, 40px);
  --blur-1: 0.6px; --blur-2: 1.2px; --blur-3: 2.2px;
  --past-scale: 0.97;
  --ease-out: cubic-bezier(0.16, 0.84, 0.3, 1);
  --ease-soft: cubic-bezier(0.22, 0.61, 0.36, 1);
  --dim: 40ms;
  --glide: 380ms;
  --ease-glide: cubic-bezier(0.61, 1, 0.88, 1);
  --gutter: 24px;
  --tail: 31vh; /* the room below the current line, which keeps it low-centre */
}
:root[data-size="-1"] { --scale: 0.88; }
:root[data-size="1"] { --scale: 1.14; }
:root[data-size="2"] { --scale: 1.3; }

*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; background: var(--paper); }
body {
  background: var(--paper); color: var(--ink);
  font-family: var(--font-ui);
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
  overflow: hidden; overscroll-behavior: none;
  transition: background-color 0.5s var(--ease-soft), color 0.5s var(--ease-soft);
}
::selection { background: var(--accent-wash); color: var(--ink); }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; margin: 0; cursor: pointer; -webkit-tap-highlight-color: transparent; }
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* the paper's texture: a fine grain of the ink's own color, alpha only */
.grain {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: var(--ink); opacity: var(--grain);
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  1.5 0 0 0 -0.62'/></filter><rect width='180' height='180' filter='url(%23g)'/></svg>") 0 0 / 180px 180px repeat;
          mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  1.5 0 0 0 -0.62'/></filter><rect width='180' height='180' filter='url(%23g)'/></svg>") 0 0 / 180px 180px repeat;
}

/* ── the now-playing bar ────────────────────────────────────────────────────────────────────────────────────── */
.bar {
  position: absolute; top: 0; left: 0; right: 0; z-index: 4;
  display: flex; align-items: center; gap: 12px;
  padding: calc(env(safe-area-inset-top) + 12px) max(16px, env(safe-area-inset-right)) 12px max(18px, env(safe-area-inset-left));
}
.brand { flex: 1; min-width: 0; display: flex; align-items: center; gap: 12px; }
.tile {
  flex: none; width: 42px; height: 42px; border-radius: 12px; display: grid; place-items: center;
  background: var(--paper-raised); box-shadow: inset 0 0 0 0.5px var(--rule), 0 1px 2px var(--shadow);
}
.flame { width: 20px; height: 27px; overflow: visible; }
.flame .outer { fill: var(--accent); transform-origin: 50% 92%; }
.flame .inner { fill: var(--flame-core); transform-origin: 50% 90%; }
[data-state="live"] .flame .outer { animation: flicker 3.4s ease-in-out infinite; }
[data-state="live"] .flame .inner { animation: glow 2.3s ease-in-out infinite; }
/* the flame holds still while the English can't come: the page's link lost, or a translator that isn't live */
[data-state="live"]:is([data-link="down"], [data-translator]:not([data-translator="live"])) .flame .outer,
[data-state="live"]:is([data-link="down"], [data-translator]:not([data-translator="live"])) .flame .inner { animation-play-state: paused; }
:root:not([data-state="live"]) .flame .outer { fill: none; stroke: var(--ink-mute); stroke-width: 1.3; }
:root:not([data-state="live"]) .flame .inner { opacity: 0; }
.name { min-width: 0; }
.wordmark {
  display: block; font-family: var(--font-caption); font-weight: 600; font-size: 20px; line-height: 1.05;
  letter-spacing: -0.01em; color: var(--ink); font-optical-sizing: auto;
}
.status {
  display: flex; align-items: center; gap: 0.5em; margin-top: 4px; white-space: nowrap;
  font-size: calc(10.5px * var(--ui-factor)); font-weight: 600; line-height: 1.2; letter-spacing: 0.15em; text-transform: uppercase;
  color: var(--ink-mute);
}
.status .on-air { color: var(--accent); }
.clock { font-variant-numeric: tabular-nums; letter-spacing: 0.08em; }
/* one status at a time: Live · 12:34, Reconnecting, Not live yet, Ended, Connecting */
.status > * { display: none; }
[data-state="live"]:not([data-link="down"]):not([data-translator]:not([data-translator="live"])) .status [data-live],
[data-state="live"][data-link="down"] .status [data-reconnecting],
[data-state="waiting"] .status [data-not-live],
[data-state="ended"] .status [data-ended],
[data-state="connecting"] .status [data-connecting] { display: inline; }
/* while live with the link up, a translator that isn't live shows its own state there instead (lumbre-pause-cue r2):
   Connecting, Reconnecting, or Ended for one that stopped or failed, which the service ends a moment later */
[data-state="live"]:not([data-link="down"])[data-translator="connecting"] .status [data-connecting],
[data-state="live"]:not([data-link="down"])[data-translator="reconnecting"] .status [data-reconnecting],
[data-state="live"]:not([data-link="down"]):is([data-translator="stopped"], [data-translator="failed"]) .status [data-ended] { display: inline; }
.status:has(.clock:empty) [data-live]:not(.on-air) { display: none; } /* no time yet: "Live" alone */

.tools { flex: none; display: flex; align-items: center; gap: 8px; }
.round {
  width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center; color: var(--ink-soft);
  box-shadow: inset 0 0 0 0.5px var(--rule); transition: transform 0.2s var(--ease-soft), color 0.2s;
}
.round:active { transform: scale(0.94); }
.round svg { width: 18px; height: 18px; }
.round .sun { display: none; }
[data-mode="dark"] .round .sun { display: block; }
[data-mode="dark"] .round .moon { display: none; }
.listen {
  height: 40px; padding: 0 16px 0 13px; border-radius: 999px; display: inline-flex; align-items: center; gap: 9px;
  font-size: calc(14.5px * var(--ui-factor)); font-weight: 600; letter-spacing: 0.01em; color: var(--ink);
  background: var(--paper-raised); box-shadow: inset 0 0 0 0.5px var(--rule), 0 1px 2px var(--shadow);
  transition: background-color 0.3s var(--ease-soft), color 0.3s var(--ease-soft), transform 0.2s var(--ease-soft);
}
.listen:active { transform: scale(0.96); }
.listen svg { width: 17px; height: 17px; }
.listen[aria-pressed="true"] { background: var(--ink); color: var(--paper); box-shadow: 0 1px 2px var(--shadow); }
.listen .bars { display: none; align-items: flex-end; gap: 2px; width: 17px; height: 13px; }
.listen .bars i { flex: 1; height: 4px; border-radius: 1px; background: currentColor; transition: opacity 0.3s var(--ease-soft); }
/* Ready: asked for before the service is live; Listening: the bars rest while the stream arrives, and move once it plays */
[data-listening="loading"] .listen .bars, [data-listening="playing"] .listen .bars { display: inline-flex; }
[data-listening="loading"] .listen .phones, [data-listening="playing"] .listen .phones { display: none; }
[data-listening="loading"] .listen .bars i { opacity: 0.55; }
[data-listening="playing"] .listen .bars i { animation: level 1.1s ease-in-out infinite; }
.listen .bars i:nth-child(2) { animation-delay: -0.45s; }
.listen .bars i:nth-child(3) { animation-delay: -0.8s; }
.listen .bars i:nth-child(4) { animation-delay: -0.2s; }

/* ── the captions ───────────────────────────────────────────────────────────────────────────────────────────── */
.stage {
  position: absolute; inset: 0; z-index: 1;
  overflow-y: auto; overflow-x: hidden; scrollbar-width: none; overscroll-behavior: contain;
  /* the clear band reaches at least to the foot of the row below the current line: a new line's first word lands there,
     to be read as the line before it dims (flow.js), and on a phone on its side that row is past the usual 80% */
  --band-foot: max(80%, calc(100% - var(--tail) + var(--caption-base) * var(--caption-factor) * var(--scale) * var(--caption-leading)));
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, transparent calc(env(safe-area-inset-top) + 74px), var(--opaque) 36%, var(--opaque) var(--band-foot), transparent 95%);
          mask-image: linear-gradient(to bottom, transparent 0, transparent calc(env(safe-area-inset-top) + 74px), var(--opaque) 36%, var(--opaque) var(--band-foot), transparent 95%);
  transition: opacity 0.9s var(--ease-soft);
}
.stage::-webkit-scrollbar { display: none; }
.lines { position: relative; max-width: 36rem; margin: 0 auto; padding: 64vh max(var(--gutter), env(safe-area-inset-right)) var(--tail) max(var(--gutter), env(safe-area-inset-left)); }
.line {
  margin: 0 0 0.62em;
  font-family: var(--font-caption); font-optical-sizing: auto;
  font-size: calc(var(--caption-base) * var(--caption-factor) * var(--scale));
  line-height: var(--caption-leading); letter-spacing: var(--caption-tracking);
  font-weight: var(--w-current); color: var(--ink);
  font-variant-numeric: oldstyle-nums proportional-nums; font-kerning: normal;
  hanging-punctuation: first allow-end last; overflow-wrap: break-word;
  /* a line grows a word at a time while it is read, so each break is decided by what comes before it alone: a word,
     once set, is never moved by the words after it (pretty and balance weigh the whole paragraph, and would) */
  text-wrap: wrap; text-wrap: stable;
  transform-origin: 0 50%;
  /* the handover is a snap (j158936): the finished line dims and blurs within about 50 ms */
  transition: color var(--dim) linear, filter var(--dim) linear, scale var(--glide) var(--ease-glide);
}
.line.is-past { font-weight: var(--w-past); scale: var(--past-scale); }
.line.frozen { white-space: nowrap; }
/* No filter here is left to paint time, where WebKit draws it through whatever small region it is repainting, and on
   iPhone stray black bars have stood beside blurred lines (r168536). So a line's blur at rest runs in the compositor, as
   its handover already does, and a word keeps nothing of its arrival once it has settled: its fill is backwards only. */
.line.is-past[data-d] { will-change: filter; }
.line.is-past[data-d="1"] { color: var(--ink-past-1); filter: blur(var(--blur-1)); }
.line.is-past[data-d="2"] { color: var(--ink-past-2); filter: blur(var(--blur-2)); }
.line.is-past[data-d="3"] { color: var(--ink-past-3); filter: blur(var(--blur-3)); }
[data-browsing="true"] .line.is-past { color: var(--ink-past-1); filter: none; scale: 1; transition-duration: 0.3s; }
.w { display: inline-block; animation: settle 0.44s var(--ease-out) backwards; }
/* a backlog's words are already there, and a new line's first words land clear (flow.js); old ones are done */
.w.still, .line.is-past[data-d="3"] .w { animation: none; }
.line.is-past.far[data-d] { filter: none; will-change: auto; } /* far back is out of view while live: no blur to keep */

/* the pause between lines: a quiet breath, as a lyrics view shows a break. It stands in the room the stack keeps below its
   lines, where the next line's first word will land, and takes none of its own: the finished line above it holds its
   place and stays the current one until that word (flow.js, lumbre-captions-flow r5). The page has one waiting sign in
   one place, so once it says "translating…" there, the cue has taken over from the dots. */
.line.is-next { height: 0; margin: 0; transition: opacity 0.45s var(--ease-soft); }
[data-pause="true"] .line.is-next { opacity: 0; }
.breath { display: inline-flex; align-items: center; gap: 0.22em; height: 1em; vertical-align: baseline; animation: appear 0.8s 0.7s var(--ease-soft) both; }
.breath i { width: 0.15em; height: 0.15em; border-radius: 50%; background: currentColor; opacity: 0.3; animation: breathe 1.8s ease-in-out infinite; }
.breath i:nth-child(2) { animation-delay: 0.22s; }
.breath i:nth-child(3) { animation-delay: 0.44s; }

/* the English on its way (lumbre-pause-cue): when the captions have been quiet a few seconds while live, a quiet word
   under the current line, gone the moment the next English arrives. It is the stack's own ::after, set in the room the
   stack keeps below its lines, so showing it moves no line; and it is generated, with no text of its own for a screen
   reader or a copy to take for something the preacher said. */
.lines::after {
  content: "translating…"; content: "translating…" / "";
  position: absolute; top: calc(100% - var(--tail)); left: max(var(--gutter), env(safe-area-inset-left));
  font-family: var(--font-caption); font-optical-sizing: auto; font-style: italic; font-weight: 400;
  font-size: calc(var(--caption-base) * var(--caption-factor) * var(--scale) * 0.56);
  line-height: 1.3; letter-spacing: 0.01em; color: var(--ink-mute);
  white-space: nowrap; pointer-events: none;
  visibility: hidden; opacity: 0;
}
[data-pause="true"] .lines::after { visibility: visible; opacity: 1; animation: appear 0.9s var(--ease-soft) both; }

/* ── the dock ───────────────────────────────────────────────────────────────────────────────────────────────── */
.dock {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 4;
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 10px;
  padding: 10px max(16px, env(safe-area-inset-right)) calc(env(safe-area-inset-bottom) + 16px) max(16px, env(safe-area-inset-left));
  pointer-events: none; transition: opacity 0.6s var(--ease-soft);
}
.dock > * { pointer-events: auto; }
.pill {
  height: 38px; display: inline-flex; align-items: center; border-radius: 999px;
  font-size: calc(13px * var(--ui-factor)); font-weight: 600; color: var(--ink-soft);
  background: var(--paper-raised); box-shadow: inset 0 0 0 0.5px var(--rule), 0 2px 10px var(--shadow);
}
.chip { justify-self: start; gap: 8px; padding: 0 13px 0 9px; }
.chip .dots { display: inline-flex; }
.chip .dots i { width: 13px; height: 13px; border-radius: 50%; box-shadow: 0 0 0 1.5px var(--paper-raised); }
.chip .dots i + i { margin-left: -4px; }
.chip .dots i:nth-child(1) { background: var(--paper-sunk); }
.chip .dots i:nth-child(2) { background: var(--ink); }
.chip .dots i:nth-child(3) { background: var(--accent); }
.chip [data-count] { color: var(--ink-mute); font-weight: 500; font-variant-numeric: tabular-nums; }
.back {
  justify-self: center; gap: 7px; padding: 0 16px 0 13px; color: var(--paper); background: var(--ink); box-shadow: 0 2px 12px var(--shadow);
  opacity: 0; translate: 0 10px; pointer-events: none; transition: opacity 0.3s var(--ease-soft), translate 0.3s var(--ease-soft);
}
.back svg { width: 14px; height: 14px; }
[data-browsing="true"] .back { opacity: 1; translate: 0 0; pointer-events: auto; }
.size { justify-self: end; padding: 0 4px; }
.size button { width: 42px; height: 38px; display: grid; place-items: center; font-family: var(--font-caption); color: var(--ink-soft); }
.size button:first-child { font-size: 14px; }
.size button:last-child { font-size: 20px; }
.size i { width: 0.5px; height: 18px; background: var(--rule); }
:root:not([data-state="live"]) .dock .back, :root:not([data-state="live"]) .dock .size { opacity: 0; pointer-events: none; }

/* ── not live yet ───────────────────────────────────────────────────────────────────────────────────────────── */
.waiting {
  position: absolute; inset: 0; z-index: 2;
  display: flex; flex-direction: column; justify-content: center;
  max-width: 36rem; margin: 0 auto;
  padding: calc(env(safe-area-inset-top) + 92px) max(28px, env(safe-area-inset-right)) calc(env(safe-area-inset-bottom) + 84px) max(28px, env(safe-area-inset-left));
  transition: opacity 0.8s var(--ease-soft), translate 0.9s var(--ease-out), visibility 0s;
}
/* hidden only once it has faded out; shown at once, so it fades in */
:root:not([data-state="waiting"]):not([data-state="ended"]) .waiting { opacity: 0; translate: 0 -18px; visibility: hidden; pointer-events: none; transition-delay: 0s, 0s, 0.9s; }
:root:not([data-state="live"]) .stage { opacity: 0; pointer-events: none; }
/* the same calm page before the service and after it, in its own words */
.waiting [data-when] { display: none; }
[data-state="waiting"] .waiting [data-when="waiting"], [data-state="ended"] .waiting [data-when="ended"] { display: revert; }
[data-state="ended"] .howto { display: none; }
.kicker {
  display: flex; align-items: center; gap: 0.7em; margin: 0;
  font-size: calc(10.5px * var(--ui-factor)); font-weight: 600; line-height: 1.3; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-mute);
}
.kicker::before { content: ""; flex: none; width: 5px; height: 5px; border-radius: 50%; background: var(--accent); }
.waiting h1 {
  margin: 18px 0 18px; font-family: var(--font-caption); font-optical-sizing: auto; font-weight: 500;
  font-size: calc(var(--caption-base) * var(--caption-factor) * 1.22); line-height: 1.06; letter-spacing: calc(var(--caption-tracking) - 0.01em);
  color: var(--ink); text-wrap: balance;
}
.waiting h1 em { font-style: italic; font-weight: 500; color: var(--accent); }
.waiting .lede {
  margin: 0 0 30px; max-width: 25em;
  font-size: calc(16.5px * var(--ui-factor)); line-height: 1.5; color: var(--ink-soft); text-wrap: pretty;
}
.waiting a { color: var(--ink); text-decoration-color: var(--rule); text-underline-offset: 0.2em; }
.howto { list-style: none; margin: 0; padding: 0; border-top: 0.5px solid var(--rule); }
.howto li {
  display: grid; grid-template-columns: 2.1em 1fr; align-items: baseline; padding: 14px 0 15px;
  border-bottom: 0.5px solid var(--hair);
  font-size: calc(16px * var(--ui-factor)); line-height: 1.4; color: var(--ink);
}
.howto b { font-family: var(--font-caption); font-weight: 500; font-size: 21px; line-height: 1; color: var(--accent); font-variant-numeric: lining-nums; }
.waiting .sign { margin: 26px 0 0; font-size: calc(12.5px * var(--ui-factor)); color: var(--ink-mute); }

@keyframes settle { from { opacity: 0; filter: blur(5px); translate: 0 0.14em; } }
@keyframes appear { from { opacity: 0; } }
@keyframes breathe { 50% { opacity: 0.85; scale: 1.25; } }
@keyframes flicker { 0%, 100% { scale: 1 1; rotate: 0deg; } 30% { scale: 1.02 0.96; rotate: -1.2deg; } 62% { scale: 0.98 1.04; rotate: 1deg; } }
@keyframes glow { 50% { opacity: 0.72; scale: 0.94 0.98; } }
@keyframes level { 0%, 100% { height: 4px; } 50% { height: 13px; } }

@media (prefers-reduced-motion: reduce) {
  .w, [data-pause="true"] .lines::after { animation: appear 0.3s ease backwards; }
  .flame .outer, .flame .inner, .breath i { animation: none !important; }
}
