*,
*::before,
*::after {
    -webkit-user-select: none;
    user-select: none;
}

html {
    height: 100%;
}

body {
    background-color: var(--yj-bg-base, black);
    color: var(--yj-text-primary, white);
    margin: 0;
    height: 100vh;
    display: grid;
    grid-template: "top-bar top-bar" 4em "sidebar main-panel" 1fr "bottom-bar bottom-bar" 4em / auto 1fr;

    /* a11y.21 (WCAG 1.4.10), measured rather than taken as filed.

       The finding's mechanism is vertical — "the 4em bars grow while
       the viewport does not, and anything that no longer fits is
       clipped with no scrollbar" — and that is not what happens. The
       middle row is `1fr`, so it absorbs the bars exactly: at 200%
       text on an 800×600 window the bars go 64px → 128px and the main
       panel goes 472px → 344px, with the footer's bottom still landing
       on 600. Nothing is clipped, and Settings stays reachable because
       the sidebar scrolls (007 phase 5).

       What is real is the other axis, which the finding does not
       mention: at 200% text the shell is 1014px wide in an 800px
       viewport, and at 320px (400% page zoom of 1280) it is 611px —
       291px of the app unreachable behind `overflow: hidden`. So the
       horizontal axis scrolls and the vertical one stays fixed, which
       is also what keeps the transport bar where a desktop player's
       transport bar belongs. At every size this app promises
       (800×600 and up, default text) there is no overflow on either
       axis and no scrollbar appears. */
    overflow-x: auto;
    overflow-y: hidden;
}



/* Above the phone breakpoint the tab bar does not exist.  It is in the
   markup unconditionally and eagerly, for the reason notification-host
   is: navigation that has to fetch a chunk before it can navigate is
   not navigation. */
@media (min-width: 600px) {
    bottom-nav {
        display: none;
    }
}

p {
    margin: 0;
    /* I want to set paragraph margins myself */
}

/* a11y.30.  Out of flow in both states, because `body` is a grid with
   named areas and an in-flow extra child is auto-placed into one of
   them — the link would silently take a row from the shell.  It is not
   `display: none`: a skip link that is not focusable is not a skip
   link. */
.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 100;
    padding: 0.5em 1em;
    background-color: var(--yj-accent);
    color: var(--yj-accent-fg);
    font-weight: 600;
    text-decoration: none;
    border-radius: 0 0 4px 0;
}

.skip-link:focus {
    left: 0;
}

/* The target of that link, so it can take focus at all. `main` is not
   focusable by default, and a fragment link to an unfocusable element
   moves the scroll and leaves the tab sequence exactly where it was —
   which is the whole thing the link exists to change. */
.main-panel:focus {
    outline: none;
}

.top-bar {
    grid-area: top-bar;
    height: 100%;
    padding-left: 2em;
    padding-right: 2em;
    display: flex;
    justify-content: space-between;
    align-items: center;
    background-color: var(--yj-bg-elevated, #343a40);
    gap: 1em;
}

.top-bar search-bar {
    flex: 0 1 320px;
}

/* What the bar gives up when it does not fit is decided by measuring
   it (`services/top-bar-fit.ts`, #143). Two rules here are what make
   that measurement mean anything.

   **Nothing but the search box may shrink.** `scrollWidth` reports a
   perfect fit while a child quietly truncates -- #69's trap, one
   component over -- and the indicator's label is `text-overflow:
   ellipsis`, so it would have absorbed the deficit and hidden it. The
   search box is exempt because it shrinks between its 320px basis and
   the 200px floor its own stylesheet sets, and a narrower input hides
   nothing it was showing. */
.top-bar hgroup,
.top-bar library-filter,
.top-bar job-indicator {
    flex-shrink: 0;
}

/* **The wordmark yields its width, not its existence.** It is the
   app's top-level heading as well as its brand, and `display: none`
   would take a document from one `h1` to none at exactly the widths
   where the view's own header is the only thing left saying where you
   are. This is `styles/sr-only.css.ts`'s recipe, written out because
   that one is a `CSSResult` for shadow roots and this is the light
   DOM. */
.top-bar hgroup.yj-collapsed {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* The bar is `justify-content: space-between`, which with four children
   spreads them evenly and left back/forward floating in the middle of
   nothing. Collecting the free space *after* this one puts the pair
   beside the brand, where a browser keeps them, and leaves the
   right-hand group exactly as it was. */
.top-bar nav-history {
    margin-right: auto;
}

ul {
    list-style-type: none;
}

.title {
    font-size: 1.5em;
    /* Both margins, not just the bottom one. The pair is flex-centred
       in a 4em bar and the UA gives an h1 a 0.67em top margin, so the
       hgroup measured 67px inside 64 and the subtitle's descenders were
       clipped by the bar. That was pre-existing; a11y.29 made it
       visible by taking the h3's bottom margin away with it, which
       shortened the block and moved the whole pair down into the clip.
       This is the state that fix landed in. */
    margin-block: 0;
}

  .subtitle {
    font-size: 0.8em;
    margin-top: 0;
  }

/* The same breakpoint the sidebar collapses at (AUTO_COLLAPSE_VIEWPORT
   in app-sidebar.ts).  The subtitle wrapped to two lines below ~780 px,
   which made the title block 98 px tall inside a 4em bar and pushed it
   down into the nav (H-11). */
@media (max-width: 899px) {
    .subtitle {
        display: none;
    }

    /* Back/forward is Desktop-band chrome (#6), and 900 is the same
       line the sidebar's labels and the subtitle are already given up
       at -- below it the shell is narrow enough that the header is
       what runs out of room first. Measured at 600, the bottom of the
       Compact band: the bar is 611px inside a 600px viewport *before*
       this component exists (filed separately), and 695px with it, so
       keeping it here would be widening a violation of the promise
       that nothing scrolls sideways at a supported size.

       Nothing is unreachable as a result, which is the rule that
       decides it: Alt+Left / Alt+Right are global and every width has
       them, the detail views keep their own back buttons, and the
       phone additionally has the platform's gesture. */
    .top-bar nav-history {
        display: none;
    }
}

body div.sidebar {
    grid-area: sidebar;
    background-color: var(--yj-bg-surface, #212529);
    overflow: hidden;
    contain: layout style paint;
}



.bottom-bar {
    grid-area: bottom-bar;
    padding: 0.25em;
    background-color: var(--yj-bg-elevated, #343a40);
    display: grid;
    /* Three columns whose outer two are the *same* width, which is what
       centres the middle one (#23). It was `var(--now-playing-width) 1fr
       auto`, so the transport's centre sat at `W/2 + 140px` — in the
       middle of the space left over, which is not the same thing and
       reads as an alignment mistake at every window size.

       The outer width is still `--now-playing-width`, so **the metadata
       panel's drag handle keeps meaning something**: widening it takes
       room from the transport on both sides at once, symmetrically. An
       `1fr … 1fr` pair would have centred the transport just as well and
       silently made that handle a no-op.

       **The cap is what stops that being a regression**, and it was
       measured as one first. Reserving the full metadata width on both
       sides costs the transport twice: at 800px the outer pair wanted
       640 of 800, and the seek bar's track went from 257px to 61px
       (and to 0 at 200% text) — the control you drag, squeezed out to
       centre the buttons above it. So the side tracks are the metadata
       width *or a quarter of the bar*, whichever is smaller, which
       leaves the drag handle meaningful everywhere it has room to be
       and hands the difference to the transport where it does not.

       `minmax(0, …)` on the outer tracks and `min-content` on the middle
       decide who yields when even that is not enough: the metadata and
       the end group shrink (both truncate; neither loses an action), and
       the transport keeps at least its buttons. Without the `min-content`
       floor the middle collapses first, because a `1fr` track's minimum
       is `auto` only until something else insists. */
    --bar-side: min(var(--now-playing-width, 320px), 25%);
    grid-template-columns:
        minmax(0, var(--bar-side))
        minmax(min-content, 1fr)
        minmax(0, var(--bar-side));
    align-items: center;
    contain: layout style;

    #now-playing-info {
        justify-self: start;
        display: flex;
        min-width: 0;
        overflow: hidden;

        #album-art {
            width: 3.5em;
            min-width: 3.5em;
            height: 3.5em;
            min-height: 3.5em;
            border-radius: 0.25em;
            background-color: var(--yj-accent, #ffd43b);
        }

        #track-info {
            display: flex;
            margin-left: 1em;
            flex-direction: column;
            font-size: 0.75em;
            justify-content: center;
            text-wrap-mode: nowrap;
            overflow: hidden;

            /* `& p`, not `p`. **A nested rule that begins with a bare
               element selector is silently dropped before Chrome 120**
               (relaxed nesting), and the phone this app runs on renders
               in Chrome 113 -- so this ellipsis, and the two rules
               below, have never applied on the device. Nothing fails;
               the text simply overflows there. The `&` form is valid in
               both, which is why it is used for every element selector
               in this file's nested blocks. */
            & p {
                overflow: hidden;
                text-overflow: ellipsis;
            }
        }
    }

    & now-playing {
        overflow: hidden;
    }

    & audio-player {
        margin: 0.5em 1em;
        min-width: 0;
    }

    /* The right-hand group, and the thing the left column is matched
       against. It is one grid cell rather than two columns because the
       centring rule above compares *columns*: volume and the queue
       button in separate tracks would make the outer pair unequal by
       whatever the volume happens to measure. */
    .bar-end {
        justify-self: end;
        display: flex;
        align-items: center;
        gap: 0.25em;
        min-width: 0;
    }

    #queue-button {
        background: none;
        border: none;
        color: inherit;
        cursor: pointer;
        padding: 8px;
        display: flex;
        align-items: center;
    }

    #queue-button:hover {
        color: var(--yj-accent, #ffd43b);
    }

    /* An open queue is a state this button can be in, and it used to
       look exactly like the closed one -- so the only way to tell what
       pressing it would do was to look at the other side of the window
       and infer it. `aria-expanded` is the same fact for anyone not
       looking at all, and it points at the panel it controls. */
    #queue-button[aria-expanded='true'] {
        color: var(--yj-accent, #ffd43b);
        background: var(--yj-bg-overlay, #404040);
        border-radius: 4px;
    }

    #queue-button.drag-over {
        color: var(--yj-accent, #ffd43b);
        outline: 2px dashed var(--yj-accent, #ffd43b);
        outline-offset: -2px;
        border-radius: 4px;
    }
}

.content-area {
    grid-area: main-panel;
    display: flex;
    overflow: hidden;
    contain: layout style;

    /* The containing block for the queue panel's overlay mode (plan
       018, #24), which spans this box rather than taking width from
       the main panel beside it. `contain: layout` already establishes
       one; this says so on purpose, so that removing the containment
       for a paint reason does not silently reparent the overlay to the
       viewport. */
    position: relative;
}

.main-panel {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    background-color: var(--yj-bg-surface, #212529);
    overflow: hidden;
    contain: layout style paint;
}

.main-panel > * {
    flex: 1;
    min-height: 0;
    box-sizing: border-box;
    contain: layout style paint;
}

/* Hidden cached views: use visibility+size collapse instead of
   display:none so scroll containers preserve their scrollTop.
   display:none discards scroll state in WebKitGTK. */
.main-panel > .view-hidden {
    visibility: hidden !important;
    flex: 0 0 0px !important;
    min-height: 0 !important;
    max-height: 0 !important;
    height: 0 !important;
    padding: 0 !important;
    margin: 0 !important;
    border: none !important;
    overflow: hidden !important;
    pointer-events: none !important;
    contain: strict !important;
}


/* ===================================================================
   The phone shell (plan 016 B2).

   **This section is last on purpose.** A media query adds no
   specificity, so `@media (max-width: 599px) { .title { … } }` placed
   above the plain `.title` rule loses to it -- which is exactly what
   happened when this landed in the middle of the file: the header kept
   its 2em gutters, its 16px gap and its 24px title on a 390px phone,
   and every one of these declarations was dead. Nothing failed,
   because the shell fits for a different reason (the `min-width: 0`
   below and each component's own media query), so a screenshot was
   what caught it.

   600px, not the sidebar's 900: 900 is a *laptop* and the response to
   it is a narrower sidebar, which is still a sidebar. Below 600 there
   is no room for one at all -- 360px of viewport over a 200px nav is
   not a layout -- so the navigation moves to the bottom, where a thumb
   is, and the eleven-item list moves into `bottom-nav`'s drawer.
   =================================================================== */
@media (max-width: 599px) {
    body {
        /* **There is no top-bar row here (#57).** Every one of the five
           things that bar held has somewhere else to be below 600px:
           `nav-history` is the platform's own gesture (gone from 899
           down), the job indicator is `<job-band>` (#62), the search
           box is a modal opened from the view's own header
           (`search-trigger`), the library filter is Settings ->
           Libraries (#148), and the wordmark is below. That is 3.25em
           of a 439 CSS px viewport -- the single biggest vertical win
           available on the reference device, which is why #57 asks for
           the row rather than for a smaller bar. */
        grid-template:
            "jobs-band" auto
            "main-panel" 1fr
            "bottom-bar" auto
            "progress-line" auto
            "bottom-nav" auto
            / 1fr;
        /* Nothing may scroll sideways here.  On a desktop the shell is
           allowed to overflow a zoomed-in window (a11y.21 above); a
           phone *is* the small viewport, so the shell has to fit it. */
        overflow-x: hidden;
    }

    body div.sidebar {
        display: none;
    }

    bottom-nav {
        grid-area: bottom-nav;
    }

    /* The bar is out of the layout, and out of it the way the *wordmark*
       already goes at desktop widths: visually hidden rather than
       `display: none`, because that `h1` is the document's top-level
       heading and this app would otherwise have none on the pages whose
       own header is empty by design (`page-header` renders no `h1` when
       `heading` is '', and Settings has no `page-header` at all).

       Its four *controls* are `display: none` below, which is what
       keeps them out of the tab order -- a visually-hidden container is
       still focusable, and tabbing into a search box nobody can see is
       worse than not having one.

       This is `styles/sr-only.css.ts`'s recipe again, written out
       because that one is a `CSSResult` for shadow roots and this is
       the light DOM. `position: absolute` is also what tells
       `services/top-bar-fit.ts` there is no row to fit into. */
    .top-bar {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
        gap: 0;
        min-width: 0;
    }

    .top-bar nav-history,
    .top-bar library-filter,
    .top-bar search-bar,
    .top-bar job-indicator {
        display: none;
    }

    /* `min-width: 0` is load-bearing wherever a box sits between the
       viewport and content that must shrink. A grid item's implicit
       minimum is `auto` -- its content -- so one child insisting on
       580px makes the *body* 580px wide inside a 360px viewport, and
       `overflow-x: hidden` then hides the right-hand third of the app
       rather than fitting it. */
    .content-area,
    .main-panel,
    .bottom-bar {
        min-width: 0;
    }

    /* The full-screen now-playing view *is* the transport, so the bar
       repeating it underneath is 4em of a small screen spent saying
       the same thing twice -- visible in a screenshot, invisible to
       every assertion about either one.

       `:has()` rather than a class toggled from index.ts: which view
       is showing is already published as an attribute, and a second
       expression of the same fact is a second thing to keep in step.
       The view carries its own queue button, because this is where
       that one lived. */
    body:has(#main-content[data-active-view="now-playing"]) .bottom-bar,
    body:has(#main-content[data-active-view="now-playing"]) player-progress-line {
        display: none;
    }
}

@media (max-width: 599px) {
    /* The phone keeps the two-part bar it had: metadata, then the
       transport and the queue button. There is no third column to
       balance because the centring the desktop does is a luxury of
       having room — at 360px the metadata needs all of the space the
       controls do not. */
    .bottom-bar {
        grid-template-columns: minmax(0, 1fr) auto auto;
        gap: 0.25em;
    }

    .bottom-bar audio-player {
        margin: 0.25em;
    }

    /* Volume stands down here whatever the setting says, because this
       is about room: five controls and a slider do not fit a 360px
       bar, and the full-screen now-playing view is where seeking and
       volume go on a phone. It moved from `audio-player`'s own media
       query when #42 moved the control into the bar — same rule, and
       now stated where the element actually is.

       **This rule used to carry the platform argument too, and no
       longer does** (#64). "The hardware keys own the volume" is not a
       width: it is false of a narrow desktop window and true of an
       Android tablet, which this selector gets backwards both ways.
       The player answers it now — `SystemOwnsVolume` — and
       `volume-control` renders nothing when it is true, at every
       width and in both of its mount points. What is left here is the
       question a stylesheet can actually answer. */
    .bottom-bar volume-control {
        display: none;
    }

    /* The queue leaves the phone's bar (#59), because #55 made it a
       screen with an entry in the back stack and Now Playing already
       carries its own button for it. The route is the mini player's
       art -> Now Playing -> the queue, which is the "reachable only
       from Now Playing" this issue asks for.

       This is allowed to remove a control only because the control is
       still reachable: plan 018's matrix promises that no action is
       ever unreachable at any supported size, and that promise is what
       `phone-transport.spec.ts` asserts rather than the button count.

       **`.bottom-bar #queue-button`, not `#queue-button`**, and that is
       not decoration. The rule this overrides is written *nested*
       inside `.bottom-bar`, so it builds to a descendant selector one
       class more specific than it looks in the source -- and a bare
       `#queue-button` here loses to it, media query or not. Being last
       in the file is not enough when the thing above is more specific,
       which is the same lesson as this section's own header one level
       down: nesting adds specificity the source does not show, and the
       failure is silent (the button simply stayed). */
    .bottom-bar #queue-button {
        display: none;
    }
}

/* Out of the desktop grid entirely.  `job-band` renders nothing above
   600px anyway, but an in-flow grid child with no named area is
   auto-placed into a row of the shell -- the same trap the skip link is
   absolutely positioned to avoid.  `player-progress-line` (#58) is the
   same element in the same position for the same reason: below 600px it
   has a named row, and above it there is no border for it to sit on --
   the desktop bar carries a real, interactive seek bar. */
body job-band,
body player-progress-line {
    display: none;
}

/* #62.  The job indicator stands down on the phone, and its work is
   shown in the notification band instead (notification-host).

   Three reasons, and the first is the report: its popover is anchored
   to the top bar, which is 3.25em here on a viewport 439 CSS px tall,
   and it was reported as unreadable behind other UI.  The second is
   that a popover is a disclosure, and background work is the one thing
   a phone should not make you disclose.  The third is #57, which
   deletes this bar entirely and is blocked on the indicator having
   somewhere else to live -- this is that somewhere.

   #57 has since done exactly that, so the indicator's own rule now
   lives with the other three in the phone block above, where the bar
   goes out of the layout in one statement rather than four. What stays
   here is the band, and the argument for it.  */
@media (max-width: 599px) {
    /* The indicator's rows appear here, in the grid row above the content.
       In flow rather than over it: a fixed band reads fine in a
       screenshot and is unusable, because at 424x439 a compact panel
       is ~200px of a 439px screen and it *covers* what is under it.
       Measured, not assumed -- four e2e specs failed on that version,
       two phone-shell journeys and the header's action menu, because
       the panel was intercepting the taps. */
    body job-band {
        display: block;
        grid-area: jobs-band;
        background-color: var(--yj-bg-elevated, #343a40);
    }
}

/* #58.  How far through the song we are, in its own grid row between
   the two bars -- so the line is *on* the border rather than inside
   either of them, and in flow rather than over it.  The row is `auto`
   and the element renders nothing while no track is loaded, so it costs
   no height at all until there is something to say.

   **This block is below the `display: none` above and has to be**, for
   the reason the band's rule is: a media query adds no specificity, so
   `body player-progress-line { display: block }` written before that
   rule loses to it at equal specificity and the line never appears at
   any width.  Nothing fails; it is simply not there. */
@media (max-width: 599px) {
    body player-progress-line {
        display: block;
        grid-area: progress-line;
    }
}
