#lyricsBtn {
    margin: 12px auto 0;
    justify-content: center;
    align-items: center;
    display: flex;
}

#lyricsBtn.hide {
    display: none;
}

/* The footer's default 12px gap separates independent notes; this line is one
   sentence, so it wants a word space instead. */
footer>.footerNote.lyricsNote {
    gap: 2px 5px;
}

#lyricsOverlay {
    position: fixed;
    inset: 0;
    z-index: 10000;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    background-color: var(--background-color);
    overflow: hidden;
}

#lyricsOverlay.hide {
    display: none;
}

/* The page behind the open overlay is out of sight, but left alone it keeps
   animating - the Apple Music card's spinning cover, its progress bar - and
   every frame of that makes the browser redraw the whole screen, the
   full-screen overlay on top included. So once the overlay has faded in,
   lyrics.js adds .lyricsOpen and the page stops being drawn and its
   animations wait where they are. visibility, not display, so nothing
   reflows and the scroll position is kept. */
html.lyricsOpen #page {
    visibility: hidden;
}

html.lyricsOpen #page * {
    animation-play-state: paused !important;
}

#lyricsOverlay.lyricsOpening {
    animation: lyricsFadeIn 0.25s ease;
}

#lyricsOverlay.lyricsClosing {
    animation: lyricsFadeOut 0.25s ease forwards;
}

/* The dialog itself takes focus when the control bar hides with a button
   focused, and there is nothing to outline when that happens. */
#lyricsOverlay:focus {
    outline: none;
}

/* ---------- Control bar ----------

   Sits across the top like a video player's controls. lyrics.js adds
   .controlsShown on any sign of activity and takes it away a few seconds
   later. Hiding it only fades it and stops it taking clicks - it is not
   visibility:hidden, because that would take the buttons away from screen
   readers too, and a VoiceOver user on an iPad has no Escape key to fall back
   on. Focus landing on a button is what brings the bar back for them.

   The buttons are the site's own .button, so they look like every other
   button. Only size and spacing are set here.
   The earlier always-glass look is kept in lyricsGlassControls.css. */
#lyricsControls {
    position: absolute;
    top: max(16px, env(safe-area-inset-top));
    left: max(16px, env(safe-area-inset-left));
    right: max(16px, env(safe-area-inset-right));
    z-index: 3;
    /* Full screen on the left, close on the right, and the song with the
       style switcher after it in the middle. The outer two columns share what
       is left equally, so the middle pair stays centred on the screen whether
       or not full screen is showing - it is only there in immersive mode. */
    display: grid;
    grid-template-columns: 1fr auto auto 1fr;
    grid-template-areas: "full song modes close";
    align-items: center;
    column-gap: 0;
    opacity: 0;
    pointer-events: none;
    transform: translateY(-10px);
    transition:
        opacity .25s ease,
        transform .25s ease;
}

#lyricsOverlay.controlsShown #lyricsControls {
    opacity: 1;
    pointer-events: auto;
    transform: none;
}

#lyricsFullscreenBtn {
    grid-area: full;
    justify-self: end;
    margin-right: 10px;
}

.lyricsModes {
    grid-area: modes;
}

#lyricsCloseBtn {
    grid-area: close;
    justify-self: start;
    margin-left: 10px;
}

/* ---------- Now playing ----------

   The cover and the song, in a box of its own to the left of the switcher.
   It is a .pillNav too - with no list inside, general.js leaves it alone - so
   it has the switcher's background, blur and rounding without restating any
   of it.

   min-width:0 lets it be narrower than a long title, which is then cut off
   with an ellipsis rather than pushing everything else aside. */
#lyricsNowPlaying {
    grid-area: song;
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
    max-width: 280px;
    margin-right: 10px;
    box-sizing: border-box;
    /* resizeNowPlaying() in lyrics.js eases it to a new song's width.
       NOW_RESIZE_MS there matches this. */
    transition: width .35s cubic-bezier(.2, .8, .2, 1);
}

/* As tall as the box's inside, whichever style sets that height. Round and
   turning like a record, the same as the cover in the Apple Music widget -
   `spin` is that widget's animation, from lanyard.css, which every page with
   the lyrics overlay loads. */
#lyricsNowArt {
    flex: 0 0 auto;
    height: 100%;
    width: auto;
    aspect-ratio: 1;
    object-fit: cover;
    border-radius: 50%;
    animation: spin 2.5s linear infinite;
    will-change: transform;
    backface-visibility: hidden;
}

/* Nobody can see it turn while the bar is faded out, so it waits there
   rather than keep animating an invisible image. */
#lyricsOverlay:not(.controlsShown) #lyricsNowArt {
    animation-play-state: paused;
}

#lyricsNowArt[hidden] {
    display: none;
}

.lyricsNowText {
    min-width: 0;
    font-family: Jost, sans-serif;
    line-height: 1.2;
}

/* Clear of the pill's rounded end, which would otherwise clip the last
   letters of a long title. */
.style-liquid-glass .lyricsNowText {
    padding-right: 12px;
}

.lyricsNowText>p {
    margin: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* The bar carries .theme-dark in immersive mode, so there these are always
   the dark theme's colours; in time-synced they follow the site's theme. */
#lyricsNowTitle {
    color: var(--text-color);
    font-size: 0.85em;
    font-weight: 550;
}

#lyricsNowArtist {
    color: var(--paragraph-color);
    font-size: 0.75em;
}

/* The style switcher is the header's navigator (.pillNav in styles.css);
   only the icon-and-label spacing inside each item is its own. The header
   zeroes the margin on icons in its items, so the label is spaced off the
   icon instead. */
.lyricsModeBtn {
    white-space: nowrap;
}

.lyricsModeBtn>span {
    margin-left: 8px;
}

/* The header colours the current item with the accent. Over album art that
   accent is often the same colour as the art itself - it was taken from it -
   so in immersive the current mode stays white and the pill is what marks
   it. */
#lyricsOverlay.isImmersive .lyricsModes li.active>.lyricsModeBtn {
    color: var(--text-color);
}

/* Round, icon-only buttons: same .button, just square with the padding
   taken out so the icon sits in the middle. */
.lyricsIconBtn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    padding: 0;
    flex: 0 0 auto;
}

.lyricsIconBtn[hidden] {
    display: none;
}

.lyricsIconBtn>i {
    margin: 0;
}

/* A phone has no room for the words, so the modes are icons alone - the
   buttons keep their names for screen readers through the hidden text. */
@media (max-width: 560px) {
    /* No room for the song beside the switcher, so they go on two rows: the
       song between full screen and close, like a video player's title, and
       the switcher centred underneath. The middle column can shrink below
       the song's width (minmax(0, ...)), so a long title is cut off with an
       ellipsis rather than pushing the buttons off the screen. Each side
       keeps room for a button and its 10px margin even when it is empty -
       full screen is only offered in immersive - so the song stays centred. */
    #lyricsControls {
        grid-template-columns: minmax(52px, 1fr) minmax(0, auto) minmax(52px, 1fr);
        grid-template-areas:
            "full song close"
            "modes modes modes";
        row-gap: 10px;
    }

    #lyricsNowPlaying {
        justify-self: center;
        max-width: 100%;
        margin-right: 0;
    }

    .lyricsModes {
        justify-self: center;
    }

    .lyricsModeBtn>span {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }
}

html.reduce-motion #lyricsControls,
html.reduce-motion #lyricsOverlay.controlsShown #lyricsControls {
    transform: none;
}

html.reduce-motion #lyricsNowPlaying {
    transition: none;
}

/* The stage scrolls, not the overlay: the active line is centred by scrolling
   this box. min-height:0 is what allows a flex child to be shorter than its
   content and actually scroll.

   The tall block padding is what lets the very first and very last lines reach
   the middle of the box, and the mask fades them out as they near the edges. */
#lyricsStage {
    /* Positioned so it is the offsetParent of its rows: scrollToActive() reads
       offsetTop expecting a distance into the scrolling content, which is only
       true if the measurement starts at this box. */
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    width: 100%;
    max-width: 900px;
    padding: 0 8vw;
    box-sizing: border-box;
    overflow-y: auto;
    scrollbar-width: none;
    display: flex;
    flex-direction: column;
    /* stretch, not center: a centred flex item shrinks to its own text, so
       every line would start at a different x and text-align would only show
       up on the ones that wrap. Full-width rows give them all one left edge. */
    align-items: stretch;
    gap: 0.55em;
    mask-image: linear-gradient(to bottom, transparent, #000 12%, #000 88%, transparent);
}

#lyricsStage::-webkit-scrollbar {
    display: none;
}

/* Half-height spacers rather than block padding. Padding counts towards a
   border-box height and is never shrunk, so 50vh top and bottom held the stage
   at a hard 100vh floor - taller than the column had to give. Flex items are
   content: they scroll
   with the lyrics instead of setting a minimum on the box. */
#lyricsStage::before,
#lyricsStage::after {
    content: "";
    flex: 0 0 50%;
}

/* Unsynced lyrics have no line to centre, so they start at the top and are
   simply read. */
#lyricsStage.isUnsynced {
    padding-block: 10vh 4vh;
    justify-content: flex-start;
}

#lyricsStage.isUnsynced::before,
#lyricsStage.isUnsynced::after {
    content: none;
}

.lyricsRow {
    font-family: Jost, sans-serif;
    font-weight: 700;
    letter-spacing: 0.01em;
    font-size: clamp(1.35em, 4.6vw, 2.5em);
    line-height: 1.2;
    text-align: left;
    /* Grows to the right off a fixed left edge, so the start of the line does
       not drift sideways as it becomes active. */
    transform-origin: left center;
    margin: 0;
    /* Dimmed until its moment: only the line being sung is at full strength,
       which is what makes the position in the song readable at a glance. */
    color: var(--paragraph-color);
    opacity: 0.45;
    transition: opacity .35s ease, color .35s ease, transform .35s ease;
}

/* The line being sung is the one thing on screen worth colouring, so it takes
   the accent: the album's own colour while the album-accent setting is on, the
   chosen theme's otherwise. --accent-text rather than --accent-color because
   this is text - that is the variable both the themes and albumAccent.js fit
   for contrast against the page background. */
.lyricsRow.isActive {
    color: var(--accent-text);
    opacity: 1;
    transform: scale(1.04);
}

/* The stand-in for a stretch with no words in it. Unlike a lyric, which stays
   in the list dimmed so the song can be read ahead of and behind, a break is
   only interesting while it is happening: out of the flow entirely the rest of
   the time, so the lines either side of it sit together. */
.lyricsRow.lyricsDots {
    display: none;
}

.lyricsRow.lyricsDots.isActive {
    display: flex;
    align-items: center;
    gap: 0.3em;
    /* Matches a line of text's height, so the break reads as one line's worth
       of silence. Held with flex:0 0 auto because the stage is a column flex
       container that overflows: a row whose min-content height is only a dot
       tall would otherwise be shrunk to exactly that. */
    flex: 0 0 auto;
    height: 1.2em;
    /* Appears in its own right rather than fading up from the dimmed state a
       lyric starts in - it was not on screen a moment ago. */
    animation: lyricsDotsIn 0.28s ease both;
}

.lyricsDot {
    width: 0.4em;
    height: 0.4em;
    border-radius: 50%;
    background-color: currentColor;
}

@keyframes lyricsDotsIn {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

/* Nothing is being timed, so nothing is dimmed - whether that is because the
   lyrics came back without timings at all, or because they have timings and the
   presence carried no start timestamp to measure them against. Without this the
   second case left every line at the dimmed 0.45 with nothing ever lit. */
#lyricsStage.isUnsynced .lyricsRow {
    opacity: 1;
    color: var(--title-color);
    font-size: clamp(1.1em, 3.4vw, 1.6em);
    font-weight: 600;
}

#lyricsStatus {
    font-family: Jost, sans-serif;
    color: var(--paragraph-color);
    font-size: 1.1em;
    text-align: center;
    margin: 0;
    padding: 0 8vw;
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
}

#lyricsStatus.hide {
    display: none;
}

@keyframes lyricsFadeIn {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

@keyframes lyricsFadeOut {
    from {
        opacity: 1;
    }

    to {
        opacity: 0;
    }
}

html.reduce-motion .lyricsRow {
    transition: none;
    transform: none;
}

html.reduce-motion .lyricsRow.isActive {
    transform: none;
}

/* The dots still mark the break; they just arrive outright. */
html.reduce-motion .lyricsRow.lyricsDots.isActive {
    animation: none;
}

html.reduce-motion #lyricsOverlay.lyricsOpening,
html.reduce-motion #lyricsOverlay.lyricsClosing {
    animation: none;
}

/* ---------- Immersive ----------

   Everything below only applies while the overlay has .isImmersive: the album
   art turned into a slowly moving blur behind everything, and the line being
   sung thrown onto the screen a word at a time. It is always dark, whatever the
   site theme, because the colours come from the album and white text is the
   only thing that reads over all of them. */

#lyricsOverlay.isImmersive {
    background-color: #101010;
    color-scheme: dark;
}

/* Drawn at 64px by lyrics.js and stretched to the screen here. Scaled past the
   edges because the outermost pixels of the blur are the least blended, and
   a hard line of colour would show along the border. */
#lyricsBackdrop {
    display: none;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 0;
    /* BACKDROP_SCALE in lyrics.js has to match, for the shade drawn into it. */
    transform: scale(1.2);
}

#lyricsOverlay.isImmersive #lyricsBackdrop {
    display: block;
}

/* The shade that darkens the art for the text is drawn into the canvas
   itself - see drawShade() in lyrics.js. */

/* Everything that has to sit above the backdrop. */
#lyricsOverlay.isImmersive #lyricsStage {
    position: relative;
    z-index: 1;
}

#lyricsOverlay.isImmersive #lyricsStatus {
    z-index: 1;
    color: rgba(255, 255, 255, 0.8);
}

/* Like a video, the pointer gets out of the picture along with the controls. */
#lyricsOverlay.isImmersive:not(.controlsShown) {
    cursor: none;
}

/* Immersive is always dark, including the time-synced list it falls back to
   when there is no start timestamp to animate with. */
#lyricsOverlay.isImmersive #lyricsStage .lyricsRow {
    color: #fff;
}

/* The stage stops being a scrolling list and becomes a fixed canvas the
   scenes are positioned on. */
#lyricsStage.isImmersive {
    display: block;
    max-width: none;
    padding: 0;
    overflow: hidden;
    mask-image: none;
}

#lyricsStage.isImmersive::before,
#lyricsStage.isImmersive::after {
    content: none;
}

/* One line of the song. --y, --tilt and --scale are picked per line by
   lyrics.js from its list of layouts. */
.immersiveScene {
    position: absolute;
    left: max(6vw, 20px);
    right: max(6vw, 20px);
    top: var(--y, 50%);
    transform: translateY(-50%) rotate(var(--tilt, 0deg));
}

.immersiveScene[data-align="start"] {
    text-align: left;
}

.immersiveScene[data-align="center"] {
    text-align: center;
}

.immersiveScene[data-align="end"] {
    text-align: right;
}

/* Rows zig-zag from one side to the other. */
.immersiveScene[data-align="stagger"] .immersiveRow:nth-child(odd) {
    text-align: left;
}

.immersiveScene[data-align="stagger"] .immersiveRow:nth-child(even) {
    text-align: right;
}

/* A slow push-in for as long as the line is up, like a camera move. Kept on
   its own element so it does not fight the scene's positioning transform. */
.immersiveInner {
    transform-origin: center;
    animation: immersivePush 12s cubic-bezier(.2, .6, .3, 1) both;
}

@keyframes immersivePush {
    from {
        transform: scale(calc(var(--scale, 1) * 0.94));
    }

    to {
        transform: scale(calc(var(--scale, 1) * 1.04));
    }
}

/* Sized by lyrics.js through --fit so each row fills about the same width,
   then capped twice by the height: 15vh so a short word on a wide screen does
   not go taller than the screen can hold, and 60vh shared between all the
   rows (--rows) so a long line broken into many rows on a phone still fits
   on the screen as a whole, clear of the control bar. */
.immersiveRow {
    font-family: Jost, sans-serif;
    font-weight: 700;
    font-size: min(calc(var(--fit, 10) * 1vw), 15vh, calc(60vh / var(--rows, 1)));
    line-height: 1.05;
    letter-spacing: -0.01em;
    color: #fff;
    margin: 0;
}

/* A word waits out of sight until its moment, then arrives from whichever
   direction the scene was given. */
.immersiveWord {
    display: inline-block;
    /* Room for everything that reaches outside the word's box: the tails of
       g, j, p and y below (the rows' line-height is tight), accents above,
       and the blur itself on every side. Safari puts an element on its own
       layer while it animates a filter and cuts that layer to the element's
       box, so without this a word arrived with its tails missing and they
       only appeared once the blur had finished.

       Jost's glyphs reach about 0.2em above and below the tight line box.
       On top of that goes twice the strongest blur used below (0.14em), since
       a blur visibly spreads about twice its radius. Both are in em so they
       grow and shrink with the word - a blur in px would outgrow the padding
       on a phone, where the words are small. The negative margin takes the
       same amount back, so the words sit exactly where they did. */
    padding: 0.5em 0.3em;
    margin: -0.5em -0.3em;
    opacity: 0;
    filter: blur(0.1em);
    transform: var(--from);
    transition:
        opacity .45s ease,
        filter .6s ease,
        transform .8s cubic-bezier(.16, 1, .3, 1);
}

/* A word that names a colour, like "red" or "blue", is shown in that colour.
   lyrics.js picks the shade and hands it over as --word-color, and wraps
   only the letters in .immersiveColor so punctuation next to the word, like
   a comma or a bracket, stays white. */
.immersiveColor {
    color: var(--word-color, #fff);
}

.immersiveScene[data-enter="rise"] {
    --from: translateY(0.45em);
}

.immersiveScene[data-enter="drop"] {
    --from: translateY(-0.45em) rotate(-4deg);
}

.immersiveScene[data-enter="zoom"] {
    --from: scale(1.6);
}

.immersiveScene[data-enter="slide"] {
    --from: translateX(-0.5em);
}

/* No text-shadow on the words, deliberately: older Safari (iPadOS 17) drops
   the shadow's blur inside a filtered, scaled layer like this one and draws
   it as a sharp second copy of each word a few pixels lower. The shade over
   the backdrop gives the white text its contrast instead.

   filter: none rather than blur(0) once a word has landed, so Safari takes
   it back off the separate layer a running filter keeps it on. The
   transition still runs: a blur animates to none as it would to blur(0). */
.immersiveWord.isSung {
    opacity: 1;
    filter: none;
    transform: none;
}

/* Backing vocals are fainter than the lead. A colour with alpha rather than
   opacity, since opacity is what animates the word in and out. */
.immersiveWord.isBacking {
    color: rgba(255, 255, 255, 0.65);
}

/* On the way out the words blur away one after another, starting from the
   first, while the next line is already coming in. */
.immersiveScene.isLeaving .immersiveWord {
    opacity: 0;
    filter: blur(0.14em);
    transform: translateY(-0.25em) scale(1.08);
    transition-duration: .7s;
    transition-delay: calc(var(--i, 0) * 25ms);
}

/* The instrumental break: three dots breathing in the middle of the screen. */
.immersiveBreak {
    display: flex;
    justify-content: center;
    gap: 3vh;
    font-size: 5vh;
    animation: lyricsDotsIn 0.4s ease both;
}

.immersiveBreak.isLeaving {
    animation: lyricsFadeOut 0.4s ease forwards;
}

.immersiveDot {
    width: 0.5em;
    height: 0.5em;
    border-radius: 50%;
    background-color: #fff;
    animation: immersiveBreathe 1.6s ease-in-out infinite;
}

.immersiveDot:nth-child(2) {
    animation-delay: 0.2s;
}

.immersiveDot:nth-child(3) {
    animation-delay: 0.4s;
}

@keyframes immersiveBreathe {
    0%,
    100% {
        opacity: 0.35;
        transform: scale(0.8);
    }

    50% {
        opacity: 1;
        transform: scale(1);
    }
}

/* With motion reduced the words still appear in time with the song, they just
   appear outright - no blur, no travel, no push-in, no breathing. */
html.reduce-motion .immersiveWord {
    filter: none;
    transform: none;
    transition: none;
}

html.reduce-motion .immersiveInner {
    animation: none;
    transform: scale(var(--scale, 1));
}

html.reduce-motion .immersiveBreak,
html.reduce-motion .immersiveDot {
    animation: none;
}
