/* ============================================================
   App UI — faithful recreation of the reference player screen
   (dark, rounded type, orange accents) — all values from tokens
   ============================================================ */

.app {
  /* no nav bar: the video sits flush at the top, but still clears the
     status bar / Dynamic Island when installed as a PWA */
  padding-top: env(safe-area-inset-top);
  font-family: var(--font-app);
  background: var(--app-bg);
  color: var(--app-text);
  display: flex;
  flex-direction: column;
  height: 100%;
}

/* ---------- wordmark ----------
   A flex child of .app, above the scrolling panel rather than inside it,
   which is what keeps it still while the list moves — no position:fixed
   needed, and none wanted inside a phone frame. */
.apphead {
  flex: 0 0 auto;
  /* Centred rather than padded: symmetric padding alone still sits the
     text high, because a text box is taller above the baseline than
     below. Let flex do it against a fixed bar height. */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: var(--tap-min);
  padding: var(--sp-2) var(--sp-3) var(--sp-1);
  /* the bar carries the mark's size so the tagline can be set as a
     fraction of it — sized off the UI token instead, the tagline creeps
     up to 84% of the mark on tablets and stops reading as subordinate */
  font-size: var(--text-md);
  color: var(--app-text);
}
/* Type lives on the mark, not on the bar: the tagline below it is a
   different size, weight and case, and would inherit all three. */
.apphead__mark {
  font-size: 1em;
  font-weight: 800;
  line-height: 1.1;
  /* Caps need more air than the app's usual tracking — set solid, the
     letterforms run together and the second word stops reading. */
  text-transform: uppercase;
  letter-spacing: 0.11em;
}
/* Says what the app is for. Kept quiet — it explains the mark above it,
   so it must not compete with it: no caps, no gradient, dim ink. */
.apphead__tag {
  font-size: 0.62em;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.02em;
  color: var(--app-text-dim);
}
/* The wordmark is painted through the letterforms, sweeping the app's own
   orange into the pink the Play tab lights up in — the same two hues the
   product already uses, rather than a second brand's colours.

   `color` is set first so a browser without background-clip:text shows
   white text instead of nothing: with the fill transparent and no clip,
   the mark would vanish rather than degrade. */
.apphead__mark { color: var(--app-text); }
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .apphead__mark {
    background: linear-gradient(90deg,
                var(--app-orange-hot) 0%, var(--tab-player) 100%);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
  }
}
/* The Play tab has no room for a name: the video is the header there. */
.app[data-panel="panel-player"] .apphead { display: none; }

/* ---------- video area ---------- */
.app__video {
  position: relative;
  background: #000;
  flex: 0 0 auto;
}
/* Exact 16:9 box. The IFrame API stamps width/height ATTRIBUTES on the
   iframe, and an attribute-set height beats CSS aspect-ratio — that left
   a tall letterbox band where YouTube draws its share/logo chrome and
   stole vertical space from the captions. A padding ratio box wins. */
.yt-frame {
  position: relative;
  width: 100%;
  padding-top: 56.25%;
  background: #000;
}
.yt-frame > iframe,
.yt-frame > div {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  height: 100%;
  border: 0;
}
.app__back {
  position: absolute;
  top: var(--sp-2);
  left: var(--sp-2);
  z-index: 3;
  width: 34px; height: 34px;
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.45);
  color: var(--app-text);
  font-size: var(--text-md);
  line-height: 1;
  cursor: pointer;
}

/* channel strip + progress */
.app__channel {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  background: var(--app-surface);
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--app-text-ui);
  color: var(--app-text-dim);
  letter-spacing: var(--track-app);
  text-transform: uppercase;
  font-weight: 700;
}
.app__progress {
  height: 3px;
  background: var(--app-chip);
}
.app__progress i {
  display: block;
  height: 100%;
  width: 0%;
  background: var(--app-orange);
  transition: width 0.2s linear;
}

/* ---------- captions ---------- */
.app__captions {
  flex: 1 1 auto;
  overflow-y: auto;
  background: var(--app-surface);
  padding: var(--sp-3) 0 var(--sp-6);
  scrollbar-width: none;
}
.app__captions::-webkit-scrollbar { display: none; }

.sent {
  padding: var(--sp-3) var(--sp-4);
  cursor: pointer;
}
.sent__translit {
  display: flex;
  flex-wrap: wrap;
  gap: 0 1.1em;
  font-size: calc(var(--app-text-translit) * var(--cap-scale));
  color: var(--app-text-faint);
  letter-spacing: var(--track-app);
  margin-bottom: 2px;
  visibility: hidden;
}
.sent__text {
  /* a wrapping flex row of word-groups, TOP aligned: every stack starts
     with a phonetics line of equal height, so the source words line up
     across groups no matter which ones carry a gloss underneath (bottom
     alignment made glossed groups ride up against their neighbours) */
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  /* the word layers inside are sized in em, so they follow this one */
  font-size: calc(var(--app-text-caption) * var(--cap-scale));
  font-weight: 800;
  letter-spacing: var(--track-app);
  line-height: 1.3;
  color: var(--app-text-faint);
  transition: color 0.2s;
}
.sent__trans {
  margin-top: var(--sp-1);
  font-size: calc(var(--app-text-trans) * var(--cap-scale));
  font-weight: 600;
  letter-spacing: var(--track-app);
  color: var(--app-text-ghost);
  transition: color 0.2s;
}

/* translation still being produced — three pulsing dots in its place, so
   the row reads as "on its way" rather than "there isn't one" */
.sent__trans.is-pending {
  display: flex;
  align-items: center;
  gap: 5px;
  height: calc(var(--app-text-trans) * var(--cap-scale) * 1.2);
}
.sent__trans.is-pending i {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--app-text-faint);
  animation: trans-dots 1.2s ease-in-out infinite;
}
.sent__trans.is-pending i:nth-child(2) { animation-delay: 0.15s; }
.sent__trans.is-pending i:nth-child(3) { animation-delay: 0.30s; }
.sent.is-active .sent__trans.is-pending i { background: var(--app-orange); }

@keyframes trans-dots {
  0%, 100% { opacity: 0.2; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .sent__trans.is-pending i { animation: none; opacity: 0.45; }
}

/* active sentence */
.sent.is-active { background: var(--app-band); }
.sent.is-active .sent__translit { visibility: visible; }
.sent.is-active .sent__text { color: var(--app-text); }
.sent.is-active .sent__trans { color: var(--app-text-dim); }

/* words */
.w {
  border-radius: var(--r-sm);
  padding: 0 0.12em;
  transition: background 0.12s;
}
.w--u-teal { box-shadow: inset 0 -3px 0 var(--ul-teal); }
.w--u-red  { box-shadow: inset 0 -3px 0 var(--ul-red); }
.sent.is-active .w.is-now {
  background: var(--app-orange);
  color: var(--app-text);
  box-shadow: none;
}

/* word popup (tap-to-translate) */
.wpop {
  position: absolute;
  z-index: 9;
  transform: translate(-50%, -110%);
  background: var(--app-card);
  border: 1px solid var(--app-chip);
  border-radius: var(--r-md);
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--app-text-ui);
  letter-spacing: var(--track-app);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s;
}
.wpop.is-open { opacity: 1; }
.wpop b { color: var(--app-orange); display: block; }
.wpop span { color: var(--app-text-dim); }

.app__more {
  position: absolute;
  right: var(--sp-3);
  bottom: 160px;
  background: none;
  border: 0;
  color: var(--app-text-dim);
  font-size: var(--text-md);
  letter-spacing: 2px;
  cursor: pointer;
  z-index: 5;
}

/* ---------- controls ---------- */
.app__controls {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4) calc(var(--sp-3) + env(safe-area-inset-bottom));
  background: var(--app-bg);
}
.ctl {
  background: none;
  border: 0;
  color: var(--app-text);
  font-family: var(--font-app);
  font-weight: 800;
  font-size: var(--app-text-ui);
  letter-spacing: var(--track-app);
  cursor: pointer;
  padding: var(--sp-1);
  opacity: 0.92;
}
.ctl:active { opacity: 0.5; }
.ctl--on { color: var(--app-orange); }
.ctl svg { display: block; width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.ctl--play { margin: 0 var(--sp-2); }
.ctl--play svg { width: 44px; height: 44px; }
.app__controls .ctl--prev { margin-left: auto; }
.app__controls .ctl--restart { margin-left: auto; }

/* ============================================================
   App screens — home / vocabulary / profile / navigation
   ============================================================ */
.screen { display: none; height: 100%; flex-direction: column; }
.screen.is-on { display: flex; }

.screen__scroll {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: var(--sp-3) var(--sp-3) 120px;
  scrollbar-width: none;
}
.screen__scroll::-webkit-scrollbar { display: none; }

/* ---------- home top bar ---------- */
.top {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: calc(var(--sp-3) + env(safe-area-inset-top)) var(--sp-3) var(--sp-2);
  font-weight: 800;
  font-size: var(--text-md);
}
.top__flag { font-size: var(--text-lg); }
.top__spacer { flex: 1; }
.top__stat { display: flex; align-items: center; gap: var(--sp-1); color: var(--app-text); }
.top__ring {
  width: 1.1em; height: 1.1em;
  border: 3px solid var(--app-chip);
  border-radius: 50%;
}
.top__energy {
  display: flex; align-items: center; gap: var(--sp-1);
  background: var(--app-orange-hot);
  color: var(--app-text);
  border-radius: var(--r-sm);
  padding: 0 var(--sp-2);
}

/* ---------- content tabs ---------- */
.tabs {
  display: flex;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  overflow-x: auto;
  scrollbar-width: none;
  flex: 0 0 auto;
}
.tabs::-webkit-scrollbar { display: none; }
.tabs__item {
  background: none; border: 0;
  font-family: var(--font-app);
  font-weight: 800;
  font-size: var(--text-md);
  letter-spacing: 0.01em;
  color: var(--app-text-dim);
  padding-bottom: var(--sp-1);
  border-bottom: 3px solid transparent;
  white-space: nowrap;
  cursor: pointer;
}
.tabs__item.is-on { color: var(--app-text); border-color: var(--app-orange-hot); }

/* ---------- greeting ---------- */
.greet { text-align: center; padding: var(--sp-3) var(--sp-3) var(--sp-2); }
.greet__title { font-size: var(--text-lg); font-weight: 800; line-height: 1.25; }
.greet__sub { color: var(--app-text-dim); font-size: var(--text-base); margin-top: var(--sp-2); font-weight: 600; }

/* ---------- feed ---------- */
.feed { display: flex; flex-direction: column; gap: var(--sp-4); padding-top: var(--sp-3); }
/* An author display rule beats the [hidden] attribute, so anything given
   display here has to say it again — the third time this has bitten in
   this file (see .yt-error and .sheet). Without it the library stayed on
   screen underneath the search results, and its height pushed the
   load-more marker out of the scroll container so paging never fired. */
.feed[hidden], .results[hidden] { display: none; }
.feed__row { display: flex; gap: var(--sp-3); align-items: flex-start; cursor: pointer; }
.feed__thumb {
  width: 38%; flex: none;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--r-sm);
  background: var(--app-card);
}
.feed__title {
  font-size: var(--text-base);
  font-weight: 700;
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.feed__hint {
  text-align: center;
  color: var(--app-text-dim);
  font-weight: 600;
  padding: var(--sp-4) var(--sp-3);
}

/* ---------- search results ----------
   Same row as the library, because a result and a saved video are the
   same kind of thing seen at two moments — only the metadata is richer
   here, since you are choosing rather than returning. */
.results { display: flex; flex-direction: column; padding-bottom: var(--sp-3); }
/* The library's 24px gap suits a short list you already know. A results
   list is scanned, and there 24px between 84px rows spends a quarter of
   the screen on nothing — tighter here, with the thumbnail doing the
   separating. */
#results-feed { gap: var(--sp-2); padding-top: var(--sp-2); }
#results-feed .feed__row { align-items: center; }
.results__end {
  padding: var(--sp-3) var(--sp-3) var(--sp-4);
  text-align: center;
  color: var(--app-text-faint);
  font-size: var(--app-text-ui);
  font-weight: 600;
  min-height: 1px;
}
.results__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  padding-top: var(--sp-3);
  color: var(--app-text-dim);
  font-size: var(--app-text-ui);
  font-weight: 700;
}
.results__clear {
  flex: 0 0 auto;
  background: none;
  border: 0;
  padding: var(--sp-1) 0;
  color: var(--app-orange-hot);
  font-family: var(--font-app);
  font-size: var(--app-text-ui);
  font-weight: 800;
  cursor: pointer;
}
.feed__row--btn {
  width: 100%;
  background: none;
  border: 0;
  padding: 0;
  color: inherit;
  font-family: var(--font-app);
  text-align: left;
  -webkit-tap-highlight-color: transparent;
}
.feed__row--btn:active { opacity: 0.55; }
.feed__thumbwrap { position: relative; width: 38%; flex: none; }
.feed__thumbwrap .feed__thumb { width: 100%; display: block; }
.feed__dur {
  position: absolute;
  right: 4px; bottom: 4px;
  background: rgba(0, 0, 0, 0.8);
  color: var(--white);
  border-radius: 4px;
  padding: 0 4px;
  font-size: 0.68rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.feed__meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.feed__chan { color: var(--app-text-dim); font-size: var(--app-text-ui); font-weight: 600; }

/* ---------- import sheet ----------
   A decision needs its own surface: the library keeps scrolling behind a
   scrim while this one question — captions or not — gets answered. */
.sheet { position: absolute; inset: 0; z-index: 30; display: flex; align-items: flex-end; }
.sheet[hidden] { display: none; }
.sheet__scrim { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.66); }
.sheet__card {
  position: relative;
  width: 100%;
  background: var(--app-card);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  padding: var(--sp-3) var(--sp-3) calc(var(--sp-3) + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  max-height: 92%;
  overflow-y: auto;
}
.sheet__poster img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--r-md);
  background: var(--app-chip);
  display: block;
}
.sheet__title { font-size: var(--text-md); font-weight: 800; line-height: 1.3; }
.sheet__sub { color: var(--app-text-dim); font-size: var(--app-text-ui); font-weight: 600; }
.sheet__status {
  border-radius: var(--r-sm);
  padding: var(--sp-2);
  background: var(--app-chip);
  color: var(--app-text-dim);
  font-size: var(--app-text-ui);
  font-weight: 700;
  line-height: 1.4;
}
.sheet__status--ok  { background: rgba(62, 214, 192, 0.14); color: #6FE3D1; }
.sheet__status--bad { background: rgba(233, 106, 45, 0.14); color: #F0996A; }
.sheet__actions { display: flex; gap: var(--sp-2); padding-top: var(--sp-1); }
.sheet__cancel, .sheet__go {
  flex: 1 1 auto;
  border: 0;
  border-radius: var(--r-pill);
  padding: var(--sp-3) var(--sp-2);
  font-family: var(--font-app);
  font-size: var(--app-text-ui);
  font-weight: 800;
  letter-spacing: var(--track-app);
  cursor: pointer;
  min-height: var(--tap-min);
}
.sheet__cancel { flex: 0 0 33%; background: var(--app-chip); color: var(--app-text); }
.sheet__go { background: var(--app-orange); color: var(--white); }
.sheet__go:disabled { background: var(--app-chip); color: var(--app-text-faint); cursor: default; }

/* ---------- bottom tab bar ---------- */
.tabbar {
  position: absolute;
  left: var(--sp-2); right: var(--sp-2);
  bottom: calc(var(--sp-2) + env(safe-area-inset-bottom));
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  z-index: 20;
}
.tabbar__pill {
  flex: 1;
  display: flex;
  justify-content: space-around;
  background: rgba(28, 28, 28, 0.92);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-radius: var(--r-pill);
  padding: var(--sp-2) var(--sp-3);
}
.tabbar__item {
  background: none; border: 0;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  font-family: var(--font-app);
  font-size: var(--app-text-ui);
  font-weight: 700;
  color: var(--app-text);
  cursor: pointer;
}
.tabbar__item svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.tabbar__item.is-on { color: var(--app-orange-hot); }
.tabbar__search {
  width: 56px; height: 56px; flex: none;
  border: 0; border-radius: 50%;
  background: rgba(28, 28, 28, 0.92);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  color: var(--app-text);
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.tabbar__search svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }

/* ---------- vocabulary ---------- */
.big-title {
  font-size: var(--text-2xl);
  font-weight: 800;
  padding: calc(var(--sp-4) + env(safe-area-inset-top)) var(--sp-3) var(--sp-3);
}
.list-card {
  background: var(--app-card);
  border-radius: var(--r-md);
  margin: 0 var(--sp-3);
  overflow: hidden;
}
.list-card__row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3);
  font-weight: 700;
  /* Same size as a video title in the library — settings rows were set a
     step larger (--text-md), which read as a different app's screen and
     pushed "Auto-pause after sentence" onto two lines. */
  font-size: var(--text-base);
  border-bottom: 1px solid var(--app-chip);
}
/* also when a note follows: the note belongs to the row above it, and a
   rule between them would cut the explanation off from what it explains */
.list-card__row:last-child,
.list-card__row:has(+ .list-card__note),
.list-card__row:has(+ .sizer) { border-bottom: 0; }
/* Everything a row puts AFTER its label hugs the trailing edge. Without
   this a switch simply follows its label, so "Auto-pause after sentence"
   looked aligned only because the label happened to be long enough to
   push it there, while the shorter "Show phonetics" sat mid-row. */
.list-card__count,
.list-card__row .switch { margin-left: auto; }
.list-card__count { color: var(--app-text-dim); }
/* ---------- playback-speed pop-up ----------
   The caption-size dial, floating: a small centred card over a scrim.
   Opening it pauses the video, so the dial is read at rest, not raced. */
.speedpop { position: absolute; inset: 0; z-index: 30; }
.speedpop[hidden] { display: none; }
.speedpop__scrim { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.6); }
.speedpop__card {
  /* anchored just above the 1x key that raised it — left/bottom are set
     by player.js from the key's box, so the card tracks it across tiers */
  position: absolute;
  width: min(340px, calc(100% - 2 * var(--sp-4)));
  background: var(--app-card);
  border: 1px solid var(--app-chip);
  border-radius: var(--r-lg);
  padding: var(--sp-4) var(--sp-2) var(--sp-2);
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.5);
}
.speedpop__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-weight: 700;
  padding: 0 var(--sp-3);
  margin-bottom: var(--sp-3);
}
.speedpop__head b { color: var(--app-text-dim); font-weight: 700; }
/* the end labels are rates here, not letters — same weight, tamer sizes */
.speedpop .sizer__a--sm { font-size: 0.68rem; }
.speedpop .sizer__a--lg { font-size: 0.85rem; }

/* ---------- caption size dial ----------
   A small A and a large A with the travel between them, notched: one tick
   per step, and a taller, thicker one in the middle for the size everyone
   starts at. The centre tick is the whole point of the control — it makes
   "back to normal" something you can find by feel rather than a button. */
.sizer {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 0 var(--sp-3) var(--sp-3);
}
.sizer__a {
  flex: 0 0 auto;             /* never let the track squeeze the letters */
  color: var(--app-text-dim);
  font-weight: 800;
  line-height: 1;
}
.sizer__a--sm { font-size: 0.78rem; }
.sizer__a--lg { font-size: 1.4rem; }

.sizer__track {
  position: relative;
  flex: 1 1 auto;
  height: var(--tap-min);     /* the finger target is the whole strip */
  touch-action: none;         /* or the drag scrolls the panel instead */
  cursor: pointer;
}
.sizer__rail {
  position: absolute;
  top: 50%; left: 0; right: 0;
  height: 2px;
  margin-top: -1px;
  border-radius: 1px;
  background: var(--app-chip);
}
.sizer__tick {
  position: absolute;
  top: 50%;
  width: 2px; height: 12px;
  margin: -6px 0 0 -1px;
  border-radius: 1px;
  background: var(--app-chip);
}
/* taller than the knob is wide, so it still reads at rest — the knob
   parks on top of it at default, which is exactly when it matters most */
.sizer__tick--home {
  width: 3px; height: 36px;
  margin: -18px 0 0 -1.5px;
  background: var(--app-text-faint);
}
.sizer__knob {
  position: absolute;
  top: 50%;
  width: 26px; height: 26px;
  margin: -13px 0 0 -13px;
  border-radius: 50%;
  background: linear-gradient(180deg, #FCFCFD 0%, #9E9EA6 100%);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.6);
}
.sizer__track:focus-visible { outline: 2px solid var(--app-orange-hot); outline-offset: 4px; border-radius: var(--r-sm); }

/* explains the switch above it, so it sits INSIDE the card, under the
   last row's rule rather than floating between cards */
.list-card__note {
  padding: 0 var(--sp-3) var(--sp-3);
  margin: calc(var(--sp-2) * -1) 0 0;
  color: var(--app-text-dim);
  font-size: var(--app-text-ui);
  font-weight: 600;
  line-height: 1.45;
}

/* An empty state is a note, not a billboard: it sits on a screen with
   nothing to compete with, so it needs no size to be seen. Set a step
   above body for the line that matters and at body for the rest, with a
   measure that keeps both off a third line. */
.empty {
  text-align: center;
  padding: var(--sp-5) var(--sp-4);
  max-width: 32ch;
  margin: 0 auto;
}
.empty__icon { font-size: 40px; opacity: 0.5; }
.empty__title {
  /* body size, carried by weight rather than scale — the screen is empty,
     so nothing is competing for the eye and size buys nothing but wrapped
     lines in a phone-width column */
  font-size: var(--text-base);
  font-weight: 800;
  line-height: 1.35;
  margin-top: var(--sp-3);
}
.empty__sub {
  color: var(--app-text-dim);
  font-size: var(--app-text-ui);
  font-weight: 600;
  margin-top: var(--sp-2);
  line-height: 1.5;
}

/* ---------- profile ---------- */
.profile__head { text-align: center; padding: calc(var(--sp-5) + env(safe-area-inset-top)) var(--sp-3) var(--sp-4); }
.profile__avatar {
  width: 84px; height: 84px;
  margin: 0 auto var(--sp-3);
  border-radius: 50%;
  background: var(--app-card);
  display: flex; align-items: center; justify-content: center;
  font-size: 40px;
}
.profile__name { font-size: var(--text-lg); font-weight: 800; }
.profile__since { color: var(--app-text-dim); font-weight: 600; margin-top: var(--sp-1); }

.stats { display: flex; gap: var(--sp-2); padding: var(--sp-3); }
.stats__chip {
  flex: 1;
  background: var(--app-card);
  border-radius: var(--r-md);
  text-align: center;
  padding: var(--sp-3) var(--sp-2);
}
.stats__num { font-size: var(--text-lg); font-weight: 800; }
.stats__label { color: var(--app-text-dim); font-size: var(--app-text-ui); font-weight: 600; margin-top: 2px; }

.pro-btn {
  display: block;
  margin: var(--sp-3);
  padding: var(--sp-3);
  border: 0;
  border-radius: var(--r-md);
  background: linear-gradient(90deg, var(--app-orange) 0%, #A94FD3 100%);
  color: var(--app-text);
  font-family: var(--font-app);
  font-size: var(--text-md);
  font-weight: 800;
  letter-spacing: var(--track-app);
  cursor: pointer;
  width: calc(100% - 2 * var(--sp-3));
}

/* ---------- player extras ---------- */
.app__preparing {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--app-text-dim);
  font-weight: 600;
  padding: var(--sp-4);
}
.app__preparing i {
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--app-orange);
  animation: pulse 1s infinite alternate;
}
@keyframes pulse { from { opacity: 0.3; } to { opacity: 1; } }

/* ============================================================
   One-page layout (Language Reactor structure, reference-app skin)
   ============================================================ */

/* sentence timeline segments */
/* ---------- scrubber ----------
   Replaces the per-sentence dots: a real draggable position bar. The
   track carries a generous transparent hit area so a thumb this small
   is still comfortable to grab on glass. */
.scrub {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  flex: 0 0 auto;
  touch-action: none;          /* drags scrub, never scroll the page */
}
.scrub__track {
  position: relative;
  flex: 1;
  height: var(--scrub-hit);      /* hit area */
  display: flex;
  align-items: center;
  cursor: pointer;
}
.scrub__track::before {
  content: '';
  position: absolute;
  left: 0; right: 0;
  height: var(--scrub-rail);
  border-radius: var(--r-pill);
  background: var(--app-chip);
}
.scrub__fill {
  position: absolute;
  left: 0;
  width: 0;
  height: var(--scrub-rail);
  border-radius: var(--r-pill);
  background: var(--app-orange);
}
.scrub__thumb {
  position: absolute;
  left: 0;
  width: var(--scrub-thumb);
  height: var(--scrub-thumb);
  margin-left: calc(var(--scrub-thumb) / -2);
  border-radius: 50%;
  background: var(--app-text);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
  transition: transform 0.15s;
}
.scrub.is-dragging .scrub__thumb { transform: scale(1.3); }
.scrub__time {
  font-size: var(--app-text-ui);
  font-weight: 700;
  font-variant-numeric: tabular-nums;   /* digits don't jitter while playing */
  color: var(--app-text-dim);
  min-width: 4.5em;
  text-align: right;
}

/* panels */
.panels { flex: 1 1 auto; position: relative; overflow: hidden; }
.panel {
  display: none;
  position: absolute;
  inset: 0;
  overflow-y: auto;
  scrollbar-width: none;
  padding: var(--sp-3);
  flex-direction: column;
}
.panel::-webkit-scrollbar { display: none; }
.panel.is-on { display: flex; }

/* ---------- player stage: LR-style sentence card ---------- */
#panel-player { justify-content: flex-start; }
.stage { display: flex; flex-direction: column; gap: var(--sp-3); }
.stage__empty {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--app-text-dim);
  font-weight: 600;
  padding: var(--sp-3);
}
.stage__empty i {
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--app-orange);
  animation: pulse 1s infinite alternate;
}
.stage__card {
  background: var(--app-band);
  border-radius: var(--r-lg);
  padding: var(--sp-3) var(--sp-3) var(--sp-4);
  text-align: center;
}
.stage__card .sent__translit {
  visibility: visible;
  justify-content: center;
  margin-bottom: var(--sp-2);
}
.stage__text {
  font-size: var(--app-text-caption);
  font-weight: 800;
  letter-spacing: var(--track-app);
  word-spacing: 0.2em;
  line-height: 1.6;
  color: var(--app-text);
}
.stage__trans {
  text-align: center;
  font-size: var(--app-text-trans);
  font-weight: 600;
  letter-spacing: var(--track-app);
  color: var(--app-text-dim);
  line-height: 1.55;
  padding: 0 var(--sp-3);
}

/* ---------- transport ----------
   One slab of hardware, cut into cells: a top edge catching light, a
   floor in shadow, and a seam between each key rather than a gap. Five
   cells of equal width, so the row reads as one object — spaced-out
   buttons on a flat band read as five separate things. */
.ctl-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

.transport {
  flex: 0 0 auto;
  display: flex;
  align-items: stretch;
  /* full bleed: the transport is a bar the screen ends in, not a card
     floating on it — so no side margins and no rounded ends, which would
     only draw attention to the gap they leave */
  margin: 0;
  background: linear-gradient(180deg, #2C2C2E 0%, #1D1D1F 100%);
  /* light along the top lip, shadow along the bottom, and the whole slab
     lifted off the page — the three cues that make it read as raised */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08),
              inset 0 -1px 0 rgba(0, 0, 0, 0.6),
              0 -6px 16px rgba(0, 0, 0, 0.45);   /* lifts off the captions above */
  overflow: hidden;              /* keeps the play glow inside the bar */
}
.transport .ctl {
  flex: 1 1 0;                   /* equal cells, and never shrink a key */
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: calc(var(--ctl-play) + var(--sp-3));
  padding: 0;
  position: relative;
  opacity: 1;
}
/* the seam: a dark cut with a lit edge on its far side, which is what a
   groove between two raised keys actually looks like */
.transport .ctl + .ctl {
  border-left: 1px solid rgba(0, 0, 0, 0.55);
  box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.05);
}
.transport .ctl:active { opacity: 1; background: rgba(0, 0, 0, 0.28); }

.transport .ctl svg {
  width: 26px; height: 26px;
  display: block;
  fill: url(#ctlSteel);
  stroke: none;                  /* these glyphs are shapes, not outlines */
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.6));
}
/* text keys (speed, auto-pause) are lit like the glyphs beside them */
.transport .ctl {
  color: #E9E9EE;
  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.6);
}
.transport .ctl--on { color: var(--app-orange-hot); }
/* the repeat key drops its steel for green while the loop is live —
   state you can read from the corner of an eye mid-sentence */
.transport .ctl--loop-on svg { fill: #30D158; }

/* The play key is the same steel as its neighbours — size alone marks it
   as the primary one. (It inherits the fill from .transport .ctl svg.) */
.transport .ctl--play svg {
  width: var(--ctl-play); height: var(--ctl-play);
}

/* ---------- tabs that aren't watching ----------
   Videos, Vocabulary and Settings are for choosing, reviewing and
   configuring — none of them is a place to watch. The player, its
   scrubber and its transport belong to the Play tab, so on these three
   they step aside and the content gets the whole screen; a half-shown
   video above a list is just a smaller list and a confused page.

   Which leaves Play as the only tab that watches, so the rule is now
   "every tab but Play" — kept as an explicit list rather than a :not()
   chain so a new tab has to opt in deliberately.

   Only display:none will do — the picture has to leave the layout, not
   merely turn invisible and keep its space. */
.app[data-panel="panel-videos"] .app__video,
.app[data-panel="panel-videos"] .scrub,
.app[data-panel="panel-videos"] .transport,
.app[data-panel="panel-vocab"] .app__video,
.app[data-panel="panel-vocab"] .scrub,
.app[data-panel="panel-vocab"] .transport,
.app[data-panel="panel-settings"] .app__video,
.app[data-panel="panel-settings"] .scrub,
.app[data-panel="panel-settings"] .transport { display: none; }

/* ---------- LR-style bottom icon bar ---------- */
.lrbar {
  flex: 0 0 auto;
  display: flex;
  justify-content: space-around;
  padding: var(--sp-2) var(--sp-2) calc(var(--sp-2) + env(safe-area-inset-bottom));
  background: var(--app-bg);
}
.lrbar__item {
  position: relative;
  background: none;
  border: 0;
  color: var(--app-text-dim);
  padding: var(--sp-2) var(--sp-3) var(--sp-2);
  cursor: pointer;
  border-bottom: 3px solid transparent;
}
.lrbar__item svg {
  width: var(--tab-icon); height: var(--tab-icon);
  fill: none; stroke: currentColor;
  stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
}
/* ---------- the active tab lights up ----------
   Icon goes white, an aura blooms behind it in that tab's own colour, a
   spark lifts off the corner and the rule underneath takes the same hue.
   The glow is drop-shadow rather than box-shadow on purpose: box-shadow
   traces the button's rectangle, drop-shadow traces the drawn strokes, so
   the light comes off the icon itself instead of a box around it. */
.lrbar__item { --tab-accent: var(--app-orange-hot); }
.lrbar__item[data-panel="panel-videos"]   { --tab-accent: var(--tab-videos); }
.lrbar__item[data-panel="panel-player"]   { --tab-accent: var(--tab-player); }
.lrbar__item[data-panel="panel-vocab"]    { --tab-accent: var(--tab-vocab); }
.lrbar__item[data-panel="panel-settings"] { --tab-accent: var(--tab-settings); }

.lrbar__item svg { transition: filter 0.22s ease, color 0.22s ease; }
.lrbar__item.is-on { color: var(--white); border-color: var(--tab-accent); }
.lrbar__item.is-on svg {
  /* A touch heavier while lit. These are hairline icons, and the aura
     bleeds across a 1.8px stroke until the white core disappears into its
     own halo — the icon then reads as coloured rather than as lit. */
  stroke-width: 2.2;
  filter: drop-shadow(0 0 3px var(--tab-accent))
          drop-shadow(0 0 10px var(--tab-accent));
}

/* the spark, top-right of the icon */
.lrbar__item.is-on::after {
  content: '';
  position: absolute;
  top: 4px; right: 8px;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--tab-accent);
  box-shadow: 0 0 6px 1px var(--tab-accent);
}
/* …unless the saved-words count is already sitting in that corner */
.lrbar__item.is-on:has(.lrbar__badge:not([hidden]))::after { content: none; }

@media (prefers-reduced-motion: reduce) {
  .lrbar__item svg { transition: none; }
}
.lrbar__badge {
  position: absolute;
  top: 2px; right: 4px;
  background: var(--app-orange);
  color: var(--app-text);
  font-size: 0.62rem;
  border-radius: var(--r-pill);
  padding: 1px 5px;
  font-style: normal;
}

/* ---------- vocab list ---------- */
.vocab { display: flex; flex-direction: column; gap: var(--sp-2); }
.vocab__item {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  background: var(--app-card);
  border-radius: var(--r-md);
  padding: var(--sp-2) var(--sp-3);
  font-weight: 700;
}
.vocab__word { color: var(--app-orange); }
.vocab__translit { color: var(--app-text-faint); font-size: var(--app-text-ui); }
.vocab__from { margin-left: auto; color: var(--app-text-faint); font-size: var(--app-text-ui); font-weight: 600; }

/* ---------- switch ---------- */
.switch {
  /* A control has one size. As a flex item next to a long label this was
     being shrunk — "Auto-pause after sentence" squeezed the pill to 40px
     while the knob inside stayed 22px at a fixed offset, so switched on it
     slid out past its own track. Never shrink a fixed-size control. */
  flex: 0 0 auto;
  width: 46px; height: 28px;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--app-chip);
  position: relative;
  cursor: pointer;
  transition: background 0.15s;
}
.switch i {
  position: absolute;
  top: 3px; left: 3px;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--white);
  transition: left 0.15s;
}
.switch.is-on { background: var(--app-orange); }
.switch.is-on i { left: 21px; }
/* Phonetics with auto-translation off: the pass that produces phonetics
   isn't running, so the switch can't act. Dimmed rather than hidden —
   a row that vanishes reads as a bug, and the setting is still true. */
.switch.is-locked { opacity: 0.35; cursor: not-allowed; }

/* ---------- debug HUD (?debug=1) ---------- */
.hud {
  position: absolute;
  left: var(--sp-2);
  bottom: 150px;
  z-index: 40;
  background: rgba(0, 0, 0, 0.75);
  color: #2AFF6B;
  font-family: var(--font-mono);
  font-size: 9px;
  line-height: 1.5;
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--r-sm);
  white-space: pre;
  max-width: 85%;
  overflow: hidden;
  cursor: pointer;
}

/* ============================================================
   Rolling caption list — sentences scroll up as they are spoken
   ============================================================ */
#panel-player { padding: 0; }

.sentlist {
  /* the first sentence starts pinned at the top; bottom headroom lets
     the last sentences scroll up to the top position too */
  padding: 0 0 60%;
}
/* …but headroom for nothing is just phantom scroll room. With it, an
   empty panel stays scrollable and can hide the cold-start bar off-screen. */
.sentlist:empty { padding-bottom: 0; }

/* every sentence is its own full-width section with a visible divider */
.sentlist .sent {
  padding: var(--sp-2) var(--sp-3);
  border-radius: 0;
  border-bottom: 1px solid var(--app-chip);
  transition: background 0.25s;
}
.sentlist .sent:last-child { border-bottom: 0; }

/* captions never sit against the transport bar: the list fades out
   just above it, and the last sentence can scroll fully clear of it */
#panel-player {
  -webkit-mask-image: linear-gradient(to bottom,
      #000 calc(100% - var(--fade-h)), transparent 100%);
  mask-image: linear-gradient(to bottom,
      #000 calc(100% - var(--fade-h)), transparent 100%);
}
.sentlist .sent .sent__text  { text-align: left; }
.sentlist .sent .sent__trans { text-align: left; }
.sentlist .sent__translit    { justify-content: flex-start; }

/* the section being spoken sits on a slightly lighter band */
.sentlist .sent.is-active { background: var(--app-card); }

/* ============================================================
   Back button — iOS conventions: leading edge, chevron glyph,
   tinted, 44pt tap target, safe-area aware. Sits over the video
   because YouTube's own overlay already shows title + channel.
   ============================================================ */
/* Swallows every pointer event before it reaches the embed, so YouTube's
   hover/tap chrome (title, share, logo, promoted cards) never surfaces.
   The back button and our transport sit above it. */
.yt-shield {
  position: absolute;
  inset: 0;
  z-index: 3;
}

/* Before first play the embed shows YouTube's own branded splash, which no
   embed parameter can turn off — so we cover it with the frame's thumbnail
   until playback starts. */
.yt-cover {
  position: absolute;
  inset: 0;
  z-index: 4;
  background: var(--app-bg) center/cover no-repeat;
  transition: opacity 0.25s;
}
.yt-cover.is-off { opacity: 0; pointer-events: none; }

/* playback refused by YouTube — sits above the poster */
.yt-error {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-4);
  background: var(--app-bg);
  color: var(--app-text-dim);
  font-size: var(--app-text-ui);
  line-height: 1.45;
  text-align: center;
}
/* a class-set display beats the [hidden] attribute — say it explicitly */
.yt-error[hidden] { display: none; }
.yt-error b { color: var(--app-text); font-size: var(--app-text-trans); }
.yt-error a { color: var(--app-orange); }


/* taps on captions mean "save word" / "seek" — never text selection */
.sentlist {
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

/* ============================================================
   Add-by-URL — paste any YouTube link
   ============================================================ */
.addurl {
  display: flex;
  gap: var(--sp-2);
  padding: 0 0 var(--sp-3);
}
.addurl__input {
  flex: 1;
  min-width: 0;
  background: var(--app-card);
  border: 1px solid var(--app-chip);
  border-radius: var(--r-pill);
  color: var(--app-text);
  font-family: var(--font-app);
  font-size: var(--text-sm);
  font-weight: 600;
  padding: var(--sp-2) var(--sp-3);
}
.addurl__input::placeholder { color: var(--app-text-faint); }
.addurl__input:focus { outline: none; border-color: var(--app-orange); }
.addurl__go {
  width: 40px; height: 40px;
  flex: none;
  border: 0;
  border-radius: 50%;
  background: var(--app-orange);
  color: var(--app-text);
  font-size: var(--text-md);
  font-weight: 800;
  cursor: pointer;
}
.addurl__go:disabled { opacity: 0.4; cursor: default; }
.addurl__msg {
  margin: 0 0 var(--sp-3);
  font-size: var(--app-text-ui);
  font-weight: 600;
  color: var(--app-text-dim);
  text-align: center;
}
.addurl__msg--err { color: var(--ul-red); }

/* ============================================================
   3-layer words: phonetics (top) / source (middle) / gloss (bottom)
   ============================================================ */
.wstack {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  margin: 0 0.5em 0.25em 0;
  vertical-align: bottom;
  cursor: pointer;
}
.w-ph {
  font-size: var(--cap-ph-size);
  line-height: 1.2;
  font-weight: 600;
  letter-spacing: 0.05em;
  color: var(--app-text-faint);
}
.w-gl {
  font-size: var(--cap-gloss-size);
  line-height: 1.25;
  font-weight: 500;
  font-style: italic;
  color: var(--app-text-dim);
}
.sent.is-active .w-gl { color: var(--app-orange-hot); }
/* unenriched sentences render as a plain single layer */
.sent.no-layers .w-ph, .sent.no-layers .w-gl { display: none; }
.sentlist.hide-ph .w-ph { display: none; }

/* phonetics belong to the sentence being SPOKEN — everywhere else
   they're noise, so inactive sentences show only text + meaning */
.sentlist .sent:not(.is-active) .w-ph { display: none; }
.vocab__gloss { color: var(--app-text-dim); font-style: italic; font-size: var(--app-text-ui); }


/* ---------- meaning groups: one gloss spans its words ---------- */
.wgroup {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  max-width: 100%;              /* a wide group can never push the layout */
  margin: 0 0.55em 0.25em 0;
  vertical-align: bottom;
}
.wgroup__words { display: inline-flex; flex-wrap: wrap; gap: 0.5em; }
.wgroup .wstack { margin: 0; }
.wgroup > .w-gl { margin-top: 1px; white-space: normal; max-width: 100%; }

/* ---------- landscape phones ----------
   A full-width 16:9 video is TALLER than a rotated phone's whole
   viewport (457px of video in 375px of screen) — captions got 0px.
   Cap the video and thin the chrome so at least one caption card
   stays on screen; YouTube pillarboxes itself inside the short frame. */
@media (orientation: landscape) and (max-height: 500px) {
  .app { --ctl-play: 28px; --tab-icon: 20px; }
  .yt-frame {
    padding-top: 0;
    height: 36vh;
  }
  .scrub     { padding-top: 2px; padding-bottom: 2px; }
  .transport { padding-top: 2px; padding-bottom: 2px; }
  .lrbar     { padding-top: 2px;
               padding-bottom: calc(2px + env(safe-area-inset-bottom)); }
  .lrbar__item { padding-top: var(--sp-1); padding-bottom: var(--sp-1); }
}

/* ---------- language selector (Settings → Speak) ---------- */
/* the row is a real <button> so it needs the div styling back */
.list-card__row--btn {
  width: 100%;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--app-chip);
  color: var(--app-text);
  font-family: var(--font-app);
  text-align: left;
  cursor: pointer;
}
.list-card__row--btn[aria-expanded="true"] .list-card__count { color: var(--app-orange-hot); }

.langlist { border-bottom: 1px solid var(--app-chip); }
.langlist__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-5);   /* indented under Speak */
  background: none;
  border: 0;
  color: var(--app-text-dim);
  font-family: var(--font-app);
  font-weight: 700;
  font-size: var(--text-base);      /* matches the row it hangs under */
  cursor: pointer;
}
.langlist__item b { visibility: hidden; font-weight: 800; }
.langlist__item.is-on { color: var(--app-text); }
.langlist__item.is-on b { visibility: visible; color: var(--app-orange-hot); }

/* Panels are column flexboxes that scroll. A flex child with
   overflow:hidden (the rounded cards) loses its automatic minimum size,
   so instead of overflowing the panel and creating scroll room it gets
   silently SHRUNK to fit — the expanded language list clipped the
   settings cards and there was nothing to scroll. Panel children must
   never shrink; taller-than-panel content is what scrolling is for. */
.panel > * { flex-shrink: 0; }


/* ---------- cold start ----------
   A first-ever view waits on YouTube plus the LLM. Naming each stage read
   as a system log; an indeterminate bar says the same thing in less. */
/* One row: the message, then the bar taking whatever width is left. The
   bar is the flexible half — the sentence must never be cut to fit it. */
.prep {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  /* Sits where the first caption will: the panel's own 16px already
     separates it from the scrubber, and stacking another 24px on top
     left it stranded in the middle of an empty panel. */
  padding: var(--sp-1) var(--sp-3);
  width: 100%;
  max-width: 34em;
}
.prep__label {
  font-weight: 700;
  font-size: var(--text-base);
  color: var(--app-text);
  flex: 0 1 auto;
}
/* On a phone the sentence at full size wraps to two lines and leaves the
   bar a stub. A step down keeps both on one row, which is the point. */
@media (max-width: 430px) {
  .prep__label { font-size: var(--text-sm); }
}
.prep__bar {
  /* small floor only: a wider one would steal the row back from the
     sentence and wrap it, which is exactly what this layout avoids */
  flex: 1 1 0;
  min-width: 3em;
  height: 4px;
  border-radius: 999px;
  background: var(--app-chip);
  overflow: hidden;
}
.prep__bar i {
  display: block;
  height: 100%;
  width: 40%;
  border-radius: 999px;
  background: var(--app-orange);
  animation: prep-slide 1.4s ease-in-out infinite;
}
@keyframes prep-slide {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}
@media (prefers-reduced-motion: reduce) {
  .prep__bar i { animation: none; width: 100%; opacity: 0.5; }
}
