/* Surtitle — a reading surface for a dark room.
 *
 * The governing constraint is light: every lit pixel is a distraction for the
 * person next to you and a reflection on the screen you are watching. So the
 * design is mostly absence, and the craft is in the little that remains. */

:root {
  /* True black, not #111: on an OLED phone these pixels are switched off, which
     emits nothing at all in a cinema and costs no battery across a long film. */
  --void:     #000000;
  /* Warm off-white. Pure white halates at low luminance and its blue content
     spoils the dark adaptation you need to watch the actual screen. */
  --ink:      #F0E6D6;
  /* The spoken line's live colour: --ink, cooling to pure white as the
     brightness slider approaches maximum. Set from app.js. */
  --ink-live: #F0E6D6;
  --ink-dim:  #6E6353;
  /* The single chroma on screen: the amber of an aisle light, the hue that
     interferes least with night vision. */
  --ember:    #D9903A;
  --ash:      #1A1613;
  --hair:     #2A231C;

  /* Two voices. Speech is set in a humanist sans — uniform strokes hold up at
     low luminance where a high-contrast face shimmers. Machine readings are
     mono, so a ticking timecode never reflows. */
  --font-read: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  --font-data: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --ink-alpha: 0.85;
  --chrome-fade: 420ms;

  /* Broadcast practice caps a subtitle at ~42 characters a line, which at the
     largest reading size lands near here. Past this the line stops being
     scannable in one go, so a wide monitor gets margins instead of longer text. */
  --stage-max: 56rem;
  --gutter: 0.7rem;
  --radius: 9px;

  --safe-top:    env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
}

* { box-sizing: border-box; }

/* Any class that sets `display` outranks the user-agent's [hidden] rule, so a
   hidden .button or .sheet would still be painted. One guard for the lot: now
   toggling `el.hidden` works on every element regardless of its class. */
[hidden] { display: none !important; }

html, body {
  height: 100%;
  margin: 0;
  background: var(--void);
  color: var(--ink);
  font-family: var(--font-read);
  -webkit-font-smoothing: antialiased;
  /* The only native chrome this app draws is a <select>'s own popup (the
     language picker). Without this the OS renders it in a light theme,
     which is the one bright rectangle the whole design exists to avoid. */
  color-scheme: dark;
  /* Deliberately NOT `overscroll-behavior: none` here. On the root that is
     exactly what disables Chrome Android's pull-to-refresh, and the page has
     nothing of its own to scroll, so it bought nothing and cost the gesture.
     Scroll chaining is contained on the inner scrollers instead. */
}

body {
  /* A lap-held phone: no accidental text selection, no double-tap zoom jitter,
     no long-press callout while you are trying to read. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

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

.mono { font-family: var(--font-data); font-size: 0.92em; }

:focus-visible {
  outline: 2px solid var(--ember);
  outline-offset: 3px;
}

/* ── Stage ──────────────────────────────────────────────────────────────── */

/* Full width on anything you would actually hold, capped only once the screen
   is wider than a subtitle line has any business being. max-width alone does
   this — below the cap it simply doesn't bind — so no breakpoint is needed. */
.stage {
  position: relative;
  display: grid;
  /* One explicit column, and every child pinned to it. Without this the opener
     and the reader both claim row 2, so auto-placement invents a second column
     and shunts the opener into it. minmax(0,…) keeps a long word from widening
     the track past the stage. */
  grid-template-columns: minmax(0, 1fr);
  /* One row, holding the reader. The top bar and the dock float above it, so
     the spoken line sits at the true centre of the screen instead of the
     centre of whatever the chrome left over — and it still does not jump when
     the chrome hides, because the chrome was never taking space. */
  grid-template-rows: minmax(0, 1fr);
  height: 100%;
  height: 100dvh;
  max-width: var(--stage-max);
  margin: 0 auto;
}

/* The only thing on screen that never hides. Absolutely positioned against the
   stage, not the viewport, so it stays with the column on a wide monitor.
   pointer-events: none — it must never swallow a tap meant for the page. */
.pulse {
  position: absolute;
  top: calc(var(--safe-top) + 0.6rem);
  right: var(--gutter);
  margin: 0;
  font-family: var(--font-data);
  font-size: 0.95rem;
  line-height: 1;
  color: var(--ember);
  opacity: 0.45;
  pointer-events: none;
  transition: opacity var(--chrome-fade) ease;
}

/* Dimmer once the chrome is gone: at that point the room is dark and this is
   the only lit pixel that is not the subtitle. */
.stage[data-chrome="off"] .pulse { opacity: 0.26; }
.stage:not([data-view="reader"]) .pulse { display: none; }

/* The start page has nothing to say about a filename or a clock. */
.stage:not([data-view="reader"]) .topbar { visibility: hidden; }

/* ── The chrome: present, then gone ─────────────────────────────────────── */

.topbar, .dock {
  transition: opacity var(--chrome-fade) ease, transform var(--chrome-fade) ease;
}

.stage[data-chrome="off"] .topbar {
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
}

.stage[data-chrome="off"] .dock {
  opacity: 0;
  transform: translateY(10px);
  pointer-events: none;
}

.topbar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 2;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  /* Right padding clears the pulse, which sits outside this element. */
  padding: calc(var(--safe-top) + 0.85rem) 2.5rem 0.7rem var(--gutter);
  font-family: var(--font-data);
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  color: var(--ink-dim);
}

.filename {
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-transform: uppercase;
}

.clock {
  margin: 0;
  color: var(--ember);
  font-variant-numeric: tabular-nums;
}

/* ── Reader ─────────────────────────────────────────────────────────────── */

/* Three reserved rows with the middle one auto-sized: the spoken line stays
   pinned to the vertical centre however long its neighbours run, so the eye
   never has to re-find it when the cue changes. */
.reader {
  grid-row: 1;
  grid-column: 1;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  justify-items: center;
  padding: 0 var(--gutter);
  opacity: var(--ink-alpha);
  text-align: center;
  cursor: default;
}

.stage:not([data-view="reader"]) .reader { visibility: hidden; }

/* No character cap: the line uses the width it is given, and the stage above is
   what decides how much that is. */
.line {
  margin: 0;
  max-width: 100%;
  text-wrap: balance;
  transition: opacity 160ms ease;
}

.line--now {
  grid-row: 2;
  /* A cue authored across two lines was broken there on purpose — usually two
     speakers, or a phrase the translator wanted split. Keep the break. */
  white-space: pre-line;
  font-size: clamp(1.55rem, 6.4vw, 2.6rem);
  /* 450, not 400: at low luminance the thinnest strokes drop out entirely,
     and anything heavier starts to bloom. */
  font-weight: 450;
  line-height: 1.28;
  letter-spacing: 0.004em;
  color: var(--ink-live);
}

.reader__before {
  grid-row: 1;
  grid-column: 1;
  width: 100%;
  align-self: end;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  padding-bottom: 1.6rem;
}

.line--prev {
  font-size: clamp(0.9rem, 3.3vw, 1.2rem);
  font-weight: 400;
  line-height: 1.35;
  color: var(--ink-dim);
}

/* The older of the two recedes further, so the stack reads as depth in time
   rather than as three lines of equal weight. */
.line--prev2 { opacity: 0.55; }

.stage[data-mode="current"] .reader__before { display: none; }
.stage[data-mode="one"] .line--prev2 { display: none; }

/* During a silence the last line stays, dimmed. A screen that strobes to black
   every few seconds is worse company in a dark room than a held line — and it
   also keeps confirming the app is still running.

   Only a step down, not a fade towards gone: this multiplies with --ink-alpha,
   so anything deeper undercuts the brightness the reader deliberately chose and
   makes the held line hard to read at the low settings that matter most. */
.line--now.is-held { opacity: 0.7; }

/* A new line rises into place. Time in this stack runs upward — a line that has
   been said moves up and dims — so words arriving from below is the direction
   the layout already implies.

   Only the spoken line moves. Carrying the line above along with it would put
   motion on the text you have already read, which is where the eye least needs
   to be pulled. Short and small on purpose: this fires on every cue, and past
   roughly a quarter-second a transition stops reading as arrival and starts
   being something you wait out before you can read the words.

   No fill mode: the animation must hand opacity back to the cascade when it
   ends, or a line that is later held would be pinned at full strength by a
   forwards fill. Transform is free to animate here — .line--now is a grid item
   in a fixed centre row, so nothing above or below it moves. */
@keyframes line-rise {
  from { opacity: 0; transform: translateY(0.42em); }
  to   { opacity: 1; transform: none; }
}

.line--now.is-rising {
  animation: line-rise 210ms cubic-bezier(0.22, 0.68, 0.32, 1);
}

/* ── Opener ─────────────────────────────────────────────────────────────── */

.opener {
  grid-row: 1;
  grid-column: 1;
  align-self: center;
  display: flex;
  justify-content: center;
  padding: 0 clamp(1.2rem, 6vw, 2.5rem);
}

/* The start page is a single centred card. Everything explanatory lives behind
   "How to use" — a setup screen that itself needs explaining has failed. */
.home {
  width: 100%;
  max-width: 26rem;
  text-align: center;
}

.home__title {
  margin: 0 0 0.55rem;
  font-size: clamp(1.75rem, 7vw, 2.15rem);
  font-weight: 550;
  letter-spacing: -0.02em;
  line-height: 1.12;
}

.home__sub {
  margin: 0;
  color: var(--ink-dim);
  font-size: 0.95rem;
  line-height: 1.55;
  text-wrap: pretty;
}

.home__actions {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-top: 2rem;
}

.home__note {
  margin: 1.4rem 0 0;
  color: var(--ink-dim);
  font-size: 0.8rem;
  line-height: 1.5;
}

.home__footer {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 1rem;
  margin-top: 2.2rem;
  font-family: var(--font-data);
  font-size: 0.68rem;
  letter-spacing: 0.04em;
}

.home__link {
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ink-dim);
  font: inherit;
  cursor: pointer;
}

.home__link:hover { color: var(--ink); }
.home__version { color: #4A4238; }

/* The opener belongs to the start page only — listed explicitly so a view added
   later doesn't get it by default. */
.stage[data-view="reader"] .opener,
.stage[data-view="ended"] .opener { display: none; }

.ended {
  grid-row: 1;
  grid-column: 1;
  align-self: center;
  display: flex;
  justify-content: center;
  padding: 0 clamp(1.2rem, 6vw, 2.5rem);
}

.stage:not([data-view="ended"]) .ended { display: none; }

.prose {
  overflow-y: auto;
  overscroll-behavior: contain;
  margin: 0;
  color: var(--ink-dim);
  font-size: 0.85rem;
  line-height: 1.6;
  text-align: left;
}

.prose p { margin: 0 0 0.8rem; }
.prose p:last-child { margin-bottom: 0; }
.prose strong { color: var(--ink); font-weight: 550; }

.prose dl {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.4rem 0.7rem;
  margin: 0.9rem 0 0;
  padding-top: 0.9rem;
  border-top: 1px solid var(--hair);
  align-items: baseline;
}

.prose dt { justify-self: start; }
.prose dd { margin: 0; }

kbd {
  display: inline-block;
  padding: 1px 5px;
  border: 1px solid var(--hair);
  border-radius: 4px;
  background: rgba(240, 230, 214, 0.05);
  color: var(--ink);
  font-family: var(--font-data);
  font-size: 0.72rem;
  white-space: nowrap;
}

/* ── Buttons ────────────────────────────────────────────────────────────── */

/* Softer corners than the dock, deliberately: the start page is a room with the
   lights on, the dock is an instrument you work by feel in the dark. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
  width: 100%;
  padding: 0.8rem 1.1rem;
  border: 1px solid var(--hair);
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 0.92rem;
  cursor: pointer;
  transition: background 160ms ease, border-color 160ms ease;
}

.btn:hover { background: rgba(240, 230, 214, 0.05); }
.btn:active { background: rgba(240, 230, 214, 0.09); }
/* The one button in the app that disables itself — while a search is in
   flight, so a second tap can't stack a second request behind it. Opacity
   rather than relying on the UA default, which some mobile browsers barely
   render at all. */
.btn:disabled { opacity: 0.5; cursor: default; }

.btn svg { flex: none; width: 1.05rem; height: 1.05rem; }

/* Filled in the accent rather than in white: it carries the same hierarchy the
   reference gets from a light fill, without putting a slab of white light in
   front of someone who may already be sitting in the dark. */
.btn--primary {
  background: var(--ember);
  border-color: var(--ember);
  color: var(--void);
  font-weight: 500;
}

.btn--primary:hover  { background: #E29B45; border-color: #E29B45; }
.btn--primary:active { background: #C9832F; border-color: #C9832F; }

/* ── Dock ───────────────────────────────────────────────────────────────── */

.dock {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 2;
  /* Same gutter as the reader, so the ribbon's ends line up with the text. */
  padding: 2.4rem var(--gutter) calc(var(--safe-bottom) + 0.8rem);
  /* A scrim, since the dock now floats over the text rather than sitting below
     it. Fades to nothing so it never reads as a bar with an edge. */
  background: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.95) 0%,
    rgba(0, 0, 0, 0.88) 55%,
    rgba(0, 0, 0, 0) 100%
  );
}

.stage:not([data-view="reader"]) .transport,
.stage:not([data-view="reader"]) .tray,
.stage:not([data-view="reader"]) .ribbon { display: none; }

/* The signature. A hairline of the entire film: every cue a tick, every silence
   a gap, the playhead the one bright mark. */
.ribbon {
  display: block;
  width: 100%;
  height: 26px;
  margin-bottom: 0.75rem;
  cursor: pointer;
  touch-action: none;
}

.transport {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  align-items: stretch;
  gap: 0.4rem;
}

.nudge, .play {
  appearance: none;
  border: 1px solid var(--hair);
  border-radius: 2px;
  background: transparent;
  color: var(--ink-dim);
  font-family: var(--font-data);
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  padding: 0.75rem 0.2rem;
  cursor: pointer;
}

.nudge:active { color: var(--ember); border-color: var(--ember); }

.play {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15rem;
  border-color: var(--ember);
  color: var(--ember);
}

.play__glyph { font-size: 0.95rem; line-height: 1; }
.play__label { font-size: 0.58rem; letter-spacing: 0.08em; text-transform: uppercase; }

/* Wraps rather than overflowing. The chips are nowrap with intrinsic widths, so
   a narrow phone pushed the last one off the edge instead of shrinking it —
   already true at 360px before the labels got longer. */
.tray {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.55rem;
}

/* Pressed chips: keep-awake, full screen. `.is-on` covers the mode chip, which
   cycles through three states and so isn't a pressed/unpressed toggle. */
.chip[aria-pressed="true"],
.chip.is-on {
  color: var(--ember);
  border-color: var(--ember);
}

/* Armed "New file" — one more tap discards the session. */
.chip.is-arming {
  color: var(--ember);
  border-color: var(--ember);
}

.chip {
  appearance: none;
  min-height: 2rem;
  border: 1px solid var(--hair);
  border-radius: 2px;
  background: transparent;
  color: var(--ink-dim);
  font-family: var(--font-data);
  font-size: 0.68rem;
  letter-spacing: 0.05em;
  padding: 0.5rem 0.65rem;
  cursor: pointer;
  white-space: nowrap;
}

/* Takes the rest of its row, but keeps a usable travel before the row wraps. */
.dimmer { flex: 1 1 8rem; display: flex; }

.dimmer input {
  appearance: none;
  width: 100%;
  height: 26px;
  background: transparent;
  cursor: pointer;
}

.dimmer input::-webkit-slider-runnable-track {
  height: 1px;
  background: var(--hair);
}

.dimmer input::-webkit-slider-thumb {
  appearance: none;
  width: 3px;
  height: 15px;
  margin-top: -7px;
  border-radius: 0;
  background: var(--ember);
}

.dimmer input::-moz-range-track { height: 1px; background: var(--hair); }
.dimmer input::-moz-range-thumb {
  width: 3px; height: 15px; border: 0; border-radius: 0; background: var(--ember);
}

/* Icon-only controls. Sized to the text chips beside them so the row has one
   baseline; the label lives in aria-label and title, not on screen. */
.iconchip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 1px solid var(--hair);
  border-radius: 2px;
  background: transparent;
  color: var(--ink-dim);
  cursor: pointer;
}

.iconchip svg { display: block; width: 1rem; height: 1rem; }
.iconchip:active { color: var(--ember); border-color: var(--ember); }

/* ── Modal ──────────────────────────────────────────────────────────────── */

.modal {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.2rem;
  background: rgba(0, 0, 0, 0.86);
  animation: fade 180ms ease;
}

.modal__card {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  width: 100%;
  max-width: 26rem;
  max-height: 80dvh;
  padding: 1.1rem 1.2rem 1.2rem;
  background: var(--ash);
  border: 1px solid var(--hair);
  border-radius: var(--radius);
  animation: lift 200ms ease;
}

.modal__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.modal__title {
  margin: 0;
  font-size: 1.02rem;
  font-weight: 550;
}

.modal__actions {
  display: flex;
  gap: 0.6rem;
}

.modal__actions .btn { flex: 1; }

@keyframes fade { from { opacity: 0; } }
@keyframes lift { from { transform: translateY(8px); opacity: 0; } }

/* ── Sheets ─────────────────────────────────────────────────────────────── */

/* Fixed positioning takes the sheet out of the stage, so it has to be given the
   same column back by hand. */
/* --kb is the height the virtual keyboard covers, measured by app.js and 0
   whenever no keyboard is up. It has to come off both ends: off `bottom` so the
   sheet sits on top of the keyboard rather than under it, and off `max-height`
   so a full sheet is capped against what's actually visible rather than against
   a viewport the keyboard is covering half of. On Chrome the layout viewport
   has already shrunk (interactive-widget, index.html) so --kb stays 0 and these
   are no-ops; on iOS Safari, which ignores that, --kb is doing the work. */
/* Above .topbar and .dock (both z-index: 2), below .modal (20). Without this a
   sheet is z-index: auto and the dock — absolute, bottom: 0, and carrying a
   near-opaque black scrim — paints over the bottom ~50px of it: the sheet's
   padding disappears into the scrim, the last row and the Find button are
   dimmed by it, and, because the dock is genuinely on top, it swallows taps
   meant for whatever is under there. */
.sheet {
  position: fixed;
  z-index: 10;
  bottom: var(--kb, 0px);
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: var(--stage-max);
  max-height: calc(82dvh - var(--kb, 0px));
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  padding: 1rem 1.1rem calc(var(--safe-bottom) + 1rem);
  background: var(--ash);
  border-top: 1px solid var(--hair);
  animation: rise 240ms ease;
}

/* The X half has to be carried through, or the sheet jumps out of its column
   for the length of the animation. */
@keyframes rise { from { transform: translate(-50%, 12px); opacity: 0; } }

.sheet__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.sheet__title {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 450;
}

.sheet__body {
  margin: 0;
  color: var(--ink-dim);
  font-size: 0.82rem;
  line-height: 1.5;
}

.search, .paste {
  width: 100%;
  padding: 0.7rem 0.8rem;
  border: 1px solid var(--hair);
  border-radius: 2px;
  background: var(--void);
  color: var(--ink);
  font: inherit;
  font-size: 0.95rem;
}

.paste {
  font-family: var(--font-data);
  font-size: 0.8rem;
  resize: vertical;
  -webkit-user-select: text;
  user-select: text;
}

.cuelist {
  flex: 1;
  overflow-y: auto;
  /* Pulling past the top of the list must not turn into a page reload. */
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  margin: 0;
  padding: 0;
  list-style: none;
}

.cue {
  display: grid;
  grid-template-columns: 4.9rem 1fr;
  gap: 0.7rem;
  align-items: baseline;
  width: 100%;
  padding: 0.62rem 0;
  border: 0;
  border-bottom: 1px solid var(--hair);
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 0.88rem;
  line-height: 1.4;
  text-align: left;
  cursor: pointer;
}

.cue__time {
  font-family: var(--font-data);
  font-size: 0.7rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-dim);
}

.cue.is-current .cue__time { color: var(--ember); }
.cue.is-current .cue__text { color: var(--ember); }

.cuelist__empty {
  padding: 1.4rem 0;
  color: var(--ink-dim);
  font-size: 0.85rem;
}

/* ── Find subtitles ─────────────────────────────────────────────────────── */
/* The sheet chrome (.sheet, .sheet__head/__title/__body), the query field
   (.search) and the results list's scroll containment (.cuelist,
   .cuelist__empty for its empty/error/offline states) are all reused as-is
   above. What's new here is the language picker and a result row, which have
   no equivalent elsewhere. */

.find__row {
  display: flex;
  gap: 0.5rem;
}

.find__row .search { flex: 1; min-width: 0; }

/* Source note, under the sheet's own description: whose catalogue this is. */
.find__source {
  margin: -0.35rem 0 0;
  color: var(--ink-dim);
  font-size: 0.72rem;
  line-height: 1.5;
}

.find__source a {
  color: var(--ink-dim);
  text-underline-offset: 0.2em;
}

/* The language combobox. The wrapper is the positioning context for the list,
   which is absolute so it floats over the results rather than pushing them
   down — the results are the thing you are trying to look at while changing
   language. */
.find__lang {
  position: relative;
  flex: none;
  width: 8.5rem;
}

.find__lang-input {
  width: 100%;
  padding: 0.7rem 0.5rem;
  border: 1px solid var(--hair);
  border-radius: 2px;
  background: var(--void);
  color: var(--ink);
  font: inherit;
  font-size: 0.85rem;
}

/* Wider than the field it hangs from, and right-aligned to it: language names
   are longer than the trigger needs to be, and the sheet has the room. */
.find__lang-list {
  position: absolute;
  z-index: 1;
  top: calc(100% + 0.25rem);
  right: 0;
  min-width: 11rem;
  /* max-height is set inline by positionLangList (app.js) from the room
     actually available; this is the fallback before it first runs. */
  max-height: 15rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  margin: 0;
  padding: 0.25rem 0;
  list-style: none;
  border: 1px solid var(--hair);
  border-radius: 2px;
  background: var(--ash);
}

/* Set by positionLangList when there is more room above the field than below,
   which is the case on the sheet's first open before any results are in it. */
.find__lang-list.is-above {
  top: auto;
  bottom: calc(100% + 0.25rem);
}

.find__lang-option {
  padding: 0.5rem 0.7rem;
  font-size: 0.85rem;
  cursor: pointer;
}

/* One highlight for both hover and keyboard, so the active row means the same
   thing however you got to it. */
.find__lang-option.is-active,
.find__lang-option:hover {
  background: var(--hair);
}

.find__lang-option[aria-selected="true"] {
  color: var(--ember);
}

.find__lang-empty {
  padding: 0.5rem 0.7rem;
  color: var(--ink-dim);
  font-size: 0.8rem;
}

/* Step one's rows. A film name is short, so unlike .result this stays on one
   line, with the kind pushed to the end as a dim tag. */
.film {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.8rem;
  width: 100%;
  padding: 0.72rem 0;
  border: 0;
  border-bottom: 1px solid var(--hair);
  background: transparent;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.film__name {
  font-size: 0.92rem;
  line-height: 1.35;
  overflow-wrap: break-word;
}

.film__kind {
  flex: none;
  color: var(--ink-dim);
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* The way back to the film list, doubling as the label for which film is
   showing. Reads as a header rather than a button because that is its more
   frequent job — the back arrow is the affordance. */
.find__crumb {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  width: 100%;
  padding: 0.2rem 0 0.5rem;
  border: 0;
  border-bottom: 1px solid var(--hair);
  background: transparent;
  color: var(--ink-dim);
  font: inherit;
  font-size: 0.82rem;
  text-align: left;
  cursor: pointer;
}

/* Same row rhythm as .cue (padding, hairline divider) but stacked rather than
   a two-column grid: a release name is too long to share a line with its own
   metadata, where .cue's timecode is short enough to sit beside its text. */
.result {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  width: 100%;
  padding: 0.62rem 0;
  border: 0;
  border-bottom: 1px solid var(--hair);
  background: transparent;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.result__release {
  font-size: 0.88rem;
  line-height: 1.35;
  overflow-wrap: break-word;
}

.result__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.6rem;
  color: var(--ink-dim);
  font-size: 0.72rem;
}

/* Shown exactly as SubDL reports it — uppercase here is a display choice on
   top of that, not a rewrite of the code itself. */
.result__lang { font-family: var(--font-data); text-transform: uppercase; }

.result__author {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 12rem;
}

/* Loading and failed both disable the button — matching .btn:disabled rather
   than inventing a second dimmed state, since this row is doing the same
   thing a disabled button does: telling you not to tap it right now. */
.result:disabled { opacity: 0.5; cursor: default; }

/* The fallback link carries its own bottom border as the row's real
   terminator, so the disabled button ahead of it doesn't double it up. */
.result:has(+ .result__fallback) { border-bottom: 0; }

.result__status {
  color: var(--ink-dim);
  font-size: 0.78rem;
}

/* The fallback for a failed row: a real, tappable link rather than a
   programmatic window.open(), sized like the row it replaces so it stays a
   comfortable one-handed target. Ember, deliberately — this is the one
   action left on a row that just told you it couldn't do the other one. */
.result__fallback {
  display: block;
  width: 100%;
  padding: 0.5rem 0 0.62rem;
  border-bottom: 1px solid var(--hair);
  color: var(--ember);
  font-size: 0.82rem;
  text-decoration: none;
}

.result__fallback:hover,
.result__fallback:active { text-decoration: underline; }

/* ── Toast ──────────────────────────────────────────────────────────────── */

/* Above the reading line rather than below it: the dock is already the busiest
   part of the screen, and a notice that covers the transport is a notice that
   arrives exactly when you are reaching for it. */
.toast {
  position: fixed;
  left: 50%;
  top: calc(var(--safe-top) + 3.2rem);
  transform: translateX(-50%);
  margin: 0;
  padding: 0.6rem 0.9rem;
  max-width: min(30rem, calc(100vw - 2rem));
  background: var(--ash);
  border: 1px solid var(--hair);
  border-radius: 2px;
  color: var(--ink-dim);
  font-size: 0.78rem;
  line-height: 1.45;
  text-align: center;
}

/* ── Motion ─────────────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
}

/* Wide screens are for setting the film up, not for watching it — keep the
   reading column at phone width so the line length stays honest. */
