/* ============================================================
   lyricsview.css — the one lyric rendering engine's styles

   Owns every .lv-* rule for all three surfaces: the Now Playing
   panel (.lv-panel), the karaoke stage (.lv-stage) and the theater
   side rail (.lv-side). It TAKES OVER the .k-line rules that used
   to live in styles.css, including the --p sweep gradient, which
   js/lyricsview.js replaces with a compositor-driven animation.

   Two rules run the whole file:

   1. Nothing that moves is animated through layout. The scroll is a
      FLIP — scrollTop snaps, and every nearby line runs @lv-scroll,
      translating from +delta back to 0. Activation scales the inner
      .lv-text, never the .lv-line, so the line boxes the FLIP measures
      never shift under it.

   2. The highlight is one @lv-wipe per word, started with a negative
      delay by JS and never touched again. background-size is a paint
      operation, which is only safe because it is written once per
      line rather than once per frame.
   ============================================================ */

/* ---------- keyframes ---------- */
@keyframes lv-scroll{
  from{transform:translate3d(0,var(--lv-delta,0px),0)}
  to  {transform:translate3d(0,0,0)}
}

/* The gradient's trailing edge is soft, and --lv-wipe-w is how wide that
   softness is; the animation has to overshoot by exactly that much or the
   last glyph of a word never reaches full strength. */
@keyframes lv-wipe{
  from{background-size:0% 100%}
  to  {background-size:calc(100% + var(--lv-wipe-w,.75em)) 100%}
}

@keyframes lv-fade-in{
  from{opacity:.45}
  to  {opacity:1}
}

/* The activation swell: a small overshoot, not a bounce. */
@keyframes lv-swell{
  0%  {transform:translate3d(0,0,0) scale3d(1,1,1)}
  38% {transform:translate3d(0,var(--lv-rise,-1px),0) scale3d(var(--lv-peak,1.055),var(--lv-peak,1.055),1)}
  100%{transform:translate3d(0,0,0) scale3d(var(--lv-grow,1.04),var(--lv-grow,1.04),1)}
}

@keyframes lv-gap-loop{
  from{transform:translate3d(0,0,0) scale3d(1.12,1.12,1)}
  to  {transform:translate3d(0,0,0) scale3d(.85,.85,1)}
}
@keyframes lv-gap-exit{
  0%  {transform:scale3d(.85,.85,1);opacity:1}
  35% {transform:scale3d(1.2,1.2,1);opacity:1}
  100%{transform:scale3d(0,0,1);opacity:0}
}
@keyframes lv-gap-in{
  from{transform:scale3d(0,0,1);opacity:0}
  to  {transform:scale3d(1,1,1);opacity:1}
}

/* ---------- shell ---------- */
/* The view takes over the host's padding box and becomes the scroller
   itself. js/lyricsview.js sets position:relative + overflow:hidden on the
   host, so this works identically inside #lyricsBody (which scrolls),
   #kStage (which clips under a mask) and #theaterLyricsBody. */
.lv-root{
  position:absolute;inset:0;
  --lv-anchor:.35;
  --lv-ease:cubic-bezier(.41,0,.12,.99);
  --lv-blur:.16em;
  --lv-blur-near:.085em;
  --lv-peak:1.06;
  --lv-grow:1.04;
  --lv-rise:-1px;
  --lv-lit-a:var(--tx,#fff);
  --lv-lit-b:var(--tx,#fff);
  --lv-dim:color-mix(in oklab,var(--tx,#fff) 44%,transparent);
}

.lv-scroll{
  position:relative;height:100%;overflow-y:auto;overflow-x:hidden;
  /* The browser's scroll anchoring actively fights a FLIP: it "corrects"
     for the content it thinks moved and cancels the snap. */
  overflow-anchor:none;
  scrollbar-width:none;-ms-overflow-style:none;
  -webkit-overflow-scrolling:touch;
  /* Never smooth: the engine decides between an instant snap and an
     explicit smooth scroll per event, and this would override both. */
  scroll-behavior:auto;
}
.lv-scroll::-webkit-scrollbar{width:0;height:0;display:none}

.lv-lead,.lv-tail{height:0;flex:none;pointer-events:none}

/* ---------- a line ---------- */
.lv-line{
  position:relative;
  opacity:.78;
  cursor:pointer;
  transform:translate3d(0,0,0);
  /* Only layout-free properties transition. filter and opacity are slower
     than the scroll on purpose, so a line fades rather than snaps. */
  transition:opacity .48s ease,filter .48s ease;
  contain:layout style;
  text-rendering:optimizeLegibility;
}
.lv-line.is-blank{cursor:default;opacity:0;pointer-events:none}

.lv-text{
  display:inline-block;
  transform:translate3d(0,0,0) scale3d(1,1,1);
  transform-origin:left center;
  transition:transform .34s var(--lv-ease);
}

/* Distance bands. Two blur levels, not a ramp — a per-distance ramp costs
   more paint and reads as mush rather than depth. */
.lv-line.is-near{opacity:.9}
.lv-line.is-far{opacity:.6}
.lv-line.is-past{opacity:.44}
.lv-line.is-prev{opacity:.52}
.lv-line.is-pre{opacity:1}
.lv-line.is-active{opacity:1}

.lv-blur .lv-line.is-far{filter:blur(var(--lv-blur))}
.lv-blur .lv-line.is-near,
.lv-blur .lv-line.is-prev{filter:blur(var(--lv-blur-near))}
.lv-blur .lv-line.is-active,
.lv-blur .lv-line.is-pre{filter:blur(0)}

.lv-line.is-active .lv-text{
  animation:lv-swell .52s var(--lv-ease) both;
}

/* The outgoing line dims IN STEP with the scroll rather than after it, which
   is why the handover reads as one movement instead of two. */
.lv-line.is-exiting{
  transition:
    opacity var(--lv-scroll-duration,400ms) var(--lv-ease),
    filter  var(--lv-scroll-duration,400ms) ease;
}

/* Off-screen: keep the classes, drop the cost. The wipe is deliberately not
   cancelled here — only the active line carries one, it is always on screen,
   and restarting a negative-delay animation later would restart it wrong. */
.lv-line.is-off{filter:none !important;will-change:auto !important}

/* the FLIP */
.lv-line.lv-anim,.lv-gap.lv-anim{
  animation-name:lv-scroll;
  animation-duration:var(--lv-scroll-duration,400ms);
  animation-timing-function:var(--lv-ease);
  animation-delay:var(--lv-line-delay,0ms);
  animation-fill-mode:both;
}
/* A line that is both scrolling and activating needs both animations; the
   swell lives on the child, so they never contend for one transform. */

/* ---------- a word ---------- */
/* The glyphs are transparent and the BACKGROUND is clipped to them: an
   unlit fill underneath, a gradient painted over it whose width is what
   @lv-wipe animates. Ligatures must be off or a sub-word wipe tears. */
.lv-word{
  display:inline-block;
  vertical-align:baseline;
  white-space:pre-wrap;
  color:transparent;
  background-color:var(--lv-dim);
  background-repeat:no-repeat;
  background-position:left center;
  background-size:0% 100%;
  background-image:linear-gradient(90deg,
    var(--lv-lit-a) 0%,
    var(--lv-lit-b) calc(100% - var(--lv-wipe-w,.75em)),
    transparent 100%);
  -webkit-background-clip:text;background-clip:text;
  font-variant-ligatures:none;
  font-feature-settings:'liga' 0;
  transition:background-color .5s ease;
}

/* wipe:'fade' — line-level sources in the panel and the side rail, where a
   sweep across a 14px line is noise rather than information. */
.lv-line.lv-fade .lv-word{
  background-image:none;
  background-color:var(--lv-lit-a);
  animation:lv-fade-in .2s ease-out forwards;
}

/* ---------- instrumental gap ---------- */
.lv-gap{
  display:flex;align-items:center;gap:11px;
  padding:10px 0;pointer-events:none;
  transform:translate3d(0,0,0);
}
.lv-gap i{
  width:11px;height:11px;border-radius:50%;
  background:var(--lv-dim);
  transform:scale3d(0,0,1);
  transition:transform .4s var(--lv-ease);
}
.lv-gap.is-active i{
  background:var(--lv-lit-b);
  animation:lv-gap-loop 4000ms ease-in-out infinite alternate;
  /* Negative, phase-aligned delay from JS: the dots are at their smallest
     exactly as the gap ends, so the exit bounce continues the same motion. */
  animation-delay:var(--lv-gap-delay,0ms);
}
.lv-gap.is-active i:nth-child(2){animation-delay:calc(var(--lv-gap-delay,0ms) + 90ms)}
.lv-gap.is-active i:nth-child(3){animation-delay:calc(var(--lv-gap-delay,0ms) + 180ms)}
.lv-gap.is-pre i{animation:lv-gap-in .32s var(--lv-ease) forwards}
.lv-gap.is-active.is-exiting i{
  animation:lv-gap-exit 600ms cubic-bezier(.33,1,.68,1) forwards;
  transition:none;
}

/* ---------- manual scroll ---------- */
/* While a finger owns the list it must behave like a list: 1:1, unblurred,
   no staggered delays chasing the playhead around. */
.lv-root.is-userscrolling .lv-line,
.lv-root.is-userscrolling .lv-gap{
  transition:none !important;
  filter:none !important;
  animation-name:none !important;
  --lv-line-delay:0ms !important;
}
.lv-root.is-userscrolling .lv-line:not(.is-active){opacity:.72 !important}
.lv-root.is-userscrolling .lv-text{transform:none !important}

/* ---------- unsynced / empty ---------- */
.lv-plain{
  white-space:pre-wrap;color:var(--tx-2);padding:2px 4px;
  font-size:14px;line-height:1.75;
}
.lv-empty{
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  height:100%;gap:11px;text-align:center;color:var(--tx-3);padding:20px;
}
.lv-empty svg{width:34px;height:34px;opacity:.35}
.lv-empty p{font-size:12.5px;line-height:1.6;max-width:250px}

/* ============================================================
   MODE: Now Playing panel
   ============================================================ */
.lv-panel{--lv-anchor:.36;--lv-peak:1.03;--lv-grow:1.02}
.lv-panel .lv-scroll{
  padding:0 2px;
  -webkit-mask-image:linear-gradient(180deg,transparent,#000 9%,#000 91%,transparent);
  mask-image:linear-gradient(180deg,transparent,#000 9%,#000 91%,transparent);
}
.lv-panel .lv-line{
  padding:6px 9px;border-radius:9px;
  font-size:14px;line-height:1.45;font-weight:500;letter-spacing:-.1px;
  transition:opacity .48s ease,filter .48s ease,background .2s;
}
.lv-panel .lv-line:hover{background:var(--panel)}
.lv-panel .lv-line.is-active{font-weight:650}
.lv-panel .lv-line.is-blank{height:8px;padding:0}
.lv-panel .lv-gap{justify-content:flex-start;padding:8px 9px}
.lv-panel .lv-gap i{width:8px;height:8px}

/* The LOOP chip is the panel's own affordance: it sets A-B to this line.
   Markup and the [data-line] hook match what app.js already delegates on. */
.lv-panel .loop-line{
  float:right;opacity:0;font-size:9.5px;font-weight:700;letter-spacing:.05em;
  padding:2px 7px;border-radius:99px;background:var(--a3);color:#fff;margin-left:8px;
  transition:opacity .14s var(--lv-ease);
}
.lv-panel .lv-line:hover .loop-line{opacity:1}

/* ============================================================
   MODE: karaoke stage
   The accent gradient the app already used for the sweep survives — it is
   now the wipe's lit half rather than a --p percentage written per frame.
   ============================================================ */
.lv-stage{
  --lv-anchor:.30;
  --lv-lit-a:var(--a2);
  --lv-lit-b:var(--a1);
  --lv-dim:rgba(255,255,255,.30);
  --lv-peak:1.08;
  --lv-grow:1.055;
  --lv-rise:-3px;
}
/* #kStage already carries a gradient mask; a second one would double it. */
.lv-stage .lv-scroll{padding:0 6vw;-webkit-mask-image:none;mask-image:none}
.lv-stage .lv-line{
  padding:clamp(5px,.8vh,11px) 0;
  font-size:clamp(21px,3.2vw,44px);line-height:1.22;
  font-weight:750;letter-spacing:-.5px;text-align:center;
}
.lv-stage .lv-text{transform-origin:center center;max-width:22ch}
.lv-stage .lv-line.is-active{
  filter:drop-shadow(0 4px 24px color-mix(in oklab,var(--a1) 42%,transparent));
}
.lv-stage .lv-line.is-blank{height:.4em;padding:0}
.lv-stage .lv-gap{justify-content:center;gap:13px}
.lv-stage .lv-gap i{width:13px;height:13px}
.lv-stage .lv-empty{color:var(--tx-3);max-width:420px;margin:auto}
.lv-stage .lv-empty svg{width:44px;height:44px}
.lv-stage .lv-empty p{font-size:14px;line-height:1.6;max-width:34ch}
.lv-stage .lv-plain{
  max-width:34ch;margin:0 auto;text-align:center;padding:4vh 0;
  font-size:clamp(16px,2vw,24px);line-height:1.9;color:rgba(255,255,255,.6);
}

@media (max-width:720px){
  .lv-stage .lv-text{max-width:16ch}
  .lv-stage .lv-scroll{padding:0 5vw}
}

/* ============================================================
   MODE: theater side rail
   ============================================================ */
.lv-side{--lv-anchor:.34;--lv-peak:1.03;--lv-grow:1.02}
.lv-side .lv-scroll{
  padding:0 2px;
  -webkit-mask-image:linear-gradient(180deg,transparent,#000 8%,#000 92%,transparent);
  mask-image:linear-gradient(180deg,transparent,#000 8%,#000 92%,transparent);
}
.lv-side .lv-line{
  padding:7px 10px;border-radius:9px;
  font-size:14.5px;line-height:1.5;font-weight:520;
  transition:opacity .48s ease,filter .48s ease,background .2s;
}
.lv-side .lv-line:hover{background:var(--panel)}
.lv-side .lv-line.is-active{font-weight:680}
.lv-side .lv-line.is-blank{height:8px;padding:0}
.lv-side .lv-gap{justify-content:flex-start;padding:8px 10px}
.lv-side .lv-gap i{width:9px;height:9px}

/* ============================================================
   REDUCED MOTION
   The wipe stays: it says WHERE IN THE LINE the singer is, which is
   information, not decoration. Everything that merely moves goes.
   The FLIP is disabled in JS as well — scrollTop is assigned directly —
   so nothing here is fighting a running animation.
   ============================================================ */
@media (prefers-reduced-motion:reduce){
  .lv-line,.lv-gap{transition:opacity .2s linear !important}
  .lv-line.lv-anim,.lv-gap.lv-anim{animation:none !important}
  .lv-text{transform:none !important;transition:none !important}
  .lv-line.is-active .lv-text{animation:none !important}
  .lv-blur .lv-line{filter:none !important}
  .lv-gap i{animation:none !important;transition:none}
  .lv-gap.is-active i{transform:scale3d(1,1,1);background:var(--lv-lit-b)}
  .lv-scroll{scroll-behavior:auto}
}
