/* ==========================================================================
   Overlay presentation.

   One system, two shapes. A TRAY (.sheet) docks to the bottom edge and slides;
   a DIALOG (.modal) centres and scales. Both carry a backdrop, a grab handle, a
   titled head, and a body that scrolls.

   Every overlay is a tray on a phone and a dialog on a desktop. There is no
   longer one that is a dialog at every width, and the exception that used to be
   here is worth recording: the trailer stayed centred on the argument that it is
   sized to its video's aspect ratio while a tray's job is to fill the width of a
   screen. That describes a desktop. At touch widths a tray IS the width of the
   screen, so a full-width 16:9 video fills it exactly and there is nothing to
   letterbox against — the reasoning was right about a shape and wrong about
   where it applied.

   Ported from Marquee. The comments that survive the port are the ones that
   record a decision someone would otherwise undo.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Tray
   -------------------------------------------------------------------------- */

.sheet {
    position: fixed;
    inset: 0;
    z-index: var(--z-sheet);
    display: flex;
    align-items: flex-end;
    justify-content: center;
}

.sheet__backdrop {
    position: absolute;
    inset: 0;
    background: var(--backdrop-tint);
    -webkit-backdrop-filter: var(--backdrop-blur);
    backdrop-filter: var(--backdrop-blur);
}

/* The panel clips; its body scrolls. Keeping the scroll on the body rather than
   the panel means anything pinned over the tray stays put while content moves
   under it. */
.sheet__panel {
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: 560px;
    max-height: 85vh;
    overflow: hidden;
    background: var(--surface);
    border: 1px solid var(--border);
    border-bottom: none;
    border-radius: var(--radius-xl) var(--radius-xl) 0 0;
    /* A tray is docked to the bottom edge, so it casts UPWARD. The usual
       downward offsets would throw the shadow off-screen and leave the raised
       top edge — the only edge anyone sees — flat against the page. */
    box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.36), 0 -12px 32px rgba(0, 0, 0, 0.34);
    /* Not the entrance — that belongs to the transition classes below. This is
       what the drag gesture animates against when it releases a partly-dragged
       tray, which is a different movement from opening or closing. */
    transition: transform var(--dur-base) var(--ease-standard);
}

/* For a tray holding a whole screen of content rather than a short list. */
.sheet__panel--tall {
    max-height: 92vh;
    min-height: 60vh;
}

.sheet__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* Stop at the end of the tray's own content. Without this, a flick that
       reaches the end hands the rest of the gesture to the page behind, which
       then scrolls — or, at the top, pulls to refresh — from under an overlay
       the user thought had the screen. */
    overscroll-behavior: contain;
    padding: 0 16px calc(20px + env(safe-area-inset-bottom));
}

/* The grab handle: the affordance that replaces a close button on touch.
   `touch-action: none` is what stops the browser claiming the drag as a scroll,
   and it only works because the grip is not itself the scroller — see the
   gesture in overlays.js. */
.sheet__grip {
    display: flex;
    justify-content: center;
    padding: 12px 0 0;
    cursor: grab;
    touch-action: none;
}

/* ONE handle for every tray, and its colour is derived rather than picked.
   #9aa0aa clears 3:1 — the bar for a control rather than for text — against
   BOTH surfaces this handle can land on:

       vs. the panel's own surface #2a2a2a          5.46:1
       vs. white artwork at 0.10 over it (#3f3f3f)  3.98:1

   Both, in one value, because a handle verified against one of them is the
   defect this replaces. It was #4b4f57, which is 1.75:1 on plain surface: below
   the bar on every tray in the app, for as long as there have been trays. That
   went unseen because the detail overlay — the only place anyone had looked at
   this handle, since it is the only one with a picture behind it — masked the
   artwork away from behind the handle instead. So the handle was never legible
   BECAUSE of that mask; it was legible in spite of being the wrong colour, in
   one overlay, by having its background deleted.

   Do not scope a lighter handle to the detail overlay. The handle is one
   component (same rule as .genre-item, same rule as the shared
   .sheet__head/.modal__head padding) and two trays are almost never on screen
   together, so a divergence here would never be noticed — it would just drift.

   3:1 and not 4.5:1 because it is not text. But on touch it is the only
   affordance that dismisses a tray by gesture, so it is the last thing in the
   app that should depend on which item happens to be open. */
.sheet__handle {
    width: 40px;
    height: 5px;
    border-radius: var(--radius-pill);
    background: #9aa0aa;
}

/* ONE head, worn by both shapes. Declared here rather than twice because the
   two rules had already drifted: 14px against 16px of top padding, which put
   the roulette's title 2px lower than every tray's below the same grab handle.

   The vertical padding is shared BECAUSE the handle is shared. An overlay
   carrying that handle makes a promise about where the panel begins, and the
   title is the first thing that promise is measured against; trays are opened
   one after another from the same screen, so a two-pixel difference between
   them does not read as two pixels, it reads as the trays not sitting still.

   Matching padding is only half of it. The distance the eye measures runs to
   the glyph, not to the top of the line box, so the title's half-leading is
   part of the gap — see the comment on .sheet__title, which is where the other
   half is kept.

   Only the horizontal inset differs, below, and it is not arbitrary: each head
   is inset with the body beneath it. */
.sheet__head,
.modal__head {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding-top: 14px;
    padding-bottom: 12px;
    border-bottom: 1px solid var(--border);
    margin-bottom: 12px;
    touch-action: none;
}

/* Matching .sheet__body. */
.sheet__head {
    padding-left: 16px;
    padding-right: 16px;
}

/* Deliberately the same type as .modal__head h2.

   A tray and a dialog wear the same grab handle, so they must hold their titles
   the same distance below it. Matching padding is only half of that: the
   distance the eye measures runs to the glyph, not to the top of the line box,
   so the title's half-leading is part of it. line-height is inherited rather
   than set for that reason — restoring an override here is the edit most likely
   to undo this quietly, because it changes nothing about the padding anyone
   would think to check. */
.sheet__title {
    font-weight: 600;
    font-size: 1.1rem;
}

/* A head title is bounded to ONE line, and it is stated here for both heads
   rather than on the one overlay that needed it first.

   The trailer is that overlay: it is the first head in the app whose title is
   DATA rather than a constant, and it has to be bounded because on touch the
   head is part of the drag region — a head that grows with its title is a drag
   region whose height depends on which item was opened. Every other head holds a
   short fixed string, so this changes nothing visible for them today. Stating it
   once means the next data-driven head is right by default.

   `min-width: 0` is the load-bearing declaration and the one that reads like
   boilerplate. These titles are flex children, and a flex item's default
   `min-width: auto` refuses to shrink below its own content — so without it a
   long title does not truncate, it pushes the close button out through the side
   of the panel.

   A LINE CLAMP and not `white-space: nowrap`, because the two heads are not the
   only rule in play. The detail overlay's title is data too and is deliberately
   allowed three lines by `.modal__fixed .modal-title` in index.html, which is
   (0,2,0) to this rule's (0,1,1) and therefore wins on the clamp alone. Nowrap
   sets a property that rule does not, so it would have won everywhere and
   silently flattened that title to one line. A clamp composes: this states the
   default, an overlay with the room states its own.

   NO `line-height`, here or in either title rule. The heads' matching padding is
   only half of what holds every overlay's title the same distance below the grab
   handle; the other half is the title's inherited half-leading, because the gap
   the eye measures runs to the glyph rather than to the top of the line box. An
   override here is the edit most likely to undo that quietly — it changes
   nothing about the padding anyone would think to check. */
.sheet__title,
.modal__head h2 {
    min-width: 0;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 1;
    line-clamp: 1;
    overflow: hidden;
}

/* --------------------------------------------------------------------------
   Dialog
   -------------------------------------------------------------------------- */

.modal {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
}

.modal__backdrop {
    position: absolute;
    inset: 0;
    background: var(--backdrop-tint);
    -webkit-backdrop-filter: var(--backdrop-blur);
    backdrop-filter: var(--backdrop-blur);
}

.modal__panel {
    position: relative;
    display: flex;
    flex-direction: column;
    width: min(560px, 100%);
    max-height: 88vh;
    overflow: hidden;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--elev-4);
}

.modal__panel--wide {
    width: min(900px, 100%);
}

/* Sized to a 16:9 video rather than to the screen. This modifier SIZES, and
   does nothing else.

   It also set `background: #000`, which made the trailer the one panel in the
   app not drawn from --surface: a pure black head sitting beside every other
   overlay's #2a2a2a, and a per-server accent that stopped at this overlay's
   border. Black belongs to the WELL the video sits in — a video letterboxes
   against its own container, which is a property of the medium rather than a
   theme choice — and it is scoped there, on `.trailer-container` in index.html,
   which says so at the rule. */
.modal__panel--video {
    width: min(960px, 100%);
}

.modal__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 0 20px 20px;
}

/* A body that has a PINNED REGION above it starts clear of the border between
   them, rather than flush against it.

   That border marks where the item's identity stops and its description begins.
   A heading set immediately beneath it groups with the block above instead of
   opening the one below, so the first line of the summary becomes the first
   thing that reads as content — the detail overlay's "Overview" was doing
   exactly that.

   The distance is the body's own section gap, and that replaces an earlier
   reason. It was 20px to match the body's horizontal inset — a real argument,
   and the wrong one once the body below has a stated vertical rhythm of its own.
   The border is the HEAVIEST division in that body, so it cannot also be its
   smallest gap: at 20px against 24px between sections, the first heading stood
   nearer the region it is not part of than any other heading stands to its
   neighbour. Reading --overlay-section-gap gives the body one rule with no
   exception — a heading stands one separation clear of whatever precedes it,
   whether that is another section or this border.

   Keyed on `.modal__fixed`, NOT on the detail overlay. The rule states the real
   condition — a scrolling body with a pinned region above it — so it is right
   the day a second overlay pins something, and it needs no class anyone has to
   remember to add. Naming the overlay instead would be a registry by another
   route, which is what overlays.js refuses at the top of the file.

   `+` and not `~`: the body is the immediate next sibling, and `~` would also
   match any later one. (Not the situation the `.sheet__grip ~ …head` rule is
   in, where `.modal-backdrop-art` sits between the two.)

   Overlays with no fixed region — trailer, roulette, genre, server switcher —
   do not match, and keep the spacing their own title bar already gives them. */
.modal__fixed + .modal__body {
    padding-top: var(--overlay-section-gap);
}

/* A dialog may hold content ABOVE its scrolling body that does not scroll with
   it — the detail overlay pins the item's poster and metadata there, so they
   stay in view while the summary moves.

   Three properties, and each is doing work. `flex: 0 0 auto` against the body's
   `1 1 auto` is what makes one region hold and the other absorb. `position:
   relative` gives the item's artwork a box to fill, so its extent follows the
   layout instead of a hardcoded height. `touch-action: none` puts the region in
   the drag gesture rather than leaving it a dead patch at the top of a tray —
   see the selector list in overlays.js, which must name it too.

   It must never become a scroller itself. `touch-action: none` here and on the
   grip and head inside it is honoured only while none of them is the scrolling
   container; make this one scroll and the browser silently takes the swipe back,
   with no error and no visual difference on a desktop. Bound its height by
   capping what grows inside it instead. */
.modal__fixed {
    flex: 0 0 auto;
    position: relative;
    touch-action: none;
}

/* Everything but the inset is shared with .sheet__head — see that rule in the
   tray section. This is a dialog's head, so it is inset with .modal__body. */
.modal__head {
    padding-left: 20px;
    padding-right: 20px;
}

.modal__head h2 {
    font-weight: 600;
    font-size: 1.1rem;
    margin: 0;
}

/* The grab handle is markup on every overlay but is a touch affordance only. On
   a pointer device the dialog is centred and closes with its own × button, so
   the handle would promise a drag that leads nowhere. The touch block at the
   bottom of this file turns it back on. */
.modal .sheet__grip {
    display: none;
}

.overlay__close {
    flex: 0 0 auto;
    width: 36px;
    height: 36px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: none;
    border-radius: var(--radius-pill);
    color: var(--muted);
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
    transition: background var(--dur-exit) var(--ease-standard),
        color var(--dur-exit) var(--ease-standard);
}

.overlay__close:hover,
.overlay__close:focus-visible {
    background: var(--surface-2);
    color: var(--text);
}

/* The focus manager moves focus onto the panel itself when an overlay opens, so
   its name is announced and the user tabs forward through its contents.
   Suppress the ring that would otherwise draw around the whole panel.

   Not a general opt-out, and deliberately narrow: the selector requires both
   attributes together, a combination only a dialog panel carries, so it cannot
   reach a control. A panel is announced, not operated. */
[role='dialog'][tabindex='-1']:focus,
[role='dialog'][tabindex='-1']:focus-visible {
    outline: none;
}

/* --------------------------------------------------------------------------
   Transitions

   Alpine owns WHEN (it applies these classes); the stylesheet owns HOW. That
   division is what lets one set of classes serve both shapes: a dialog and a
   tray differ in how they move, not in when, and the panel rules below key that
   difference off which panel is inside.

   `enter-start` and `leave-end` are the same class, as are `enter-end` and
   `leave-start`: an overlay has two resting appearances, shut and shown, and a
   transition is a trip between them in one direction or the other.
   -------------------------------------------------------------------------- */

.overlay-opening {
    transition: opacity var(--dur-base) var(--ease-entrance);
}

/* `pointer-events: none` is load-bearing, not polish. Alpine holds the element
   displayed for the length of the leave transition, so without it a dismissed
   overlay goes on swallowing clicks while it fades — the user dismisses it,
   reaches for something behind it, and the tap lands on a backdrop that is
   visually almost gone. */
.overlay-closing {
    transition: opacity var(--dur-exit) var(--ease-exit);
    pointer-events: none;
}

.overlay-shut {
    opacity: 0;
}

.overlay-shown {
    opacity: 1;
}

/* A dialog scales from slightly reduced. 0.96 rather than anything smaller: it
   should look like it is arriving, not like it is being thrown at the screen.
   Scaling the overlay root would take the backdrop with it, so the transform is
   on the panel and the fade is on the root. */
.overlay-opening .modal__panel {
    transition: transform var(--dur-base) var(--ease-entrance);
}

.overlay-closing .modal__panel {
    transition: transform var(--dur-exit) var(--ease-exit);
}

.overlay-shut .modal__panel {
    transform: scale(0.96);
}

.overlay-shown .modal__panel {
    transform: scale(1);
}

/* A tray slides instead, and takes longer doing it — it travels the height of
   its own panel rather than 4% of it. */
.overlay-opening .sheet__panel {
    transition: transform var(--dur-slow) var(--ease-entrance);
}

.overlay-closing .sheet__panel {
    transition: transform var(--dur-exit) var(--ease-exit);
}

.overlay-shut .sheet__panel {
    transform: translateY(100%);
}

.overlay-shown .sheet__panel {
    transform: translateY(0);
}

/* --------------------------------------------------------------------------
   Page scroll lock

   Pinning the body is the only technique that holds on iOS Safari: `overflow:
   hidden` on the body is unreliable there, and `overscroll-behavior` is not
   honoured on the document at all. overlays.js writes the class and restores
   the scroll position.
   -------------------------------------------------------------------------- */

.is-overlay-open body {
    position: fixed;
    left: 0;
    right: 0;
    width: 100%;
    overflow: hidden;
}

/* --------------------------------------------------------------------------
   Pointer presentation

   A tray is a TOUCH shape. Docked to the bottom edge, full-bleed, dismissed by
   dragging a handle — all of which assumes a thumb. On a desktop it is simply a
   panel glued to the bottom of a large screen, with a drag affordance that
   promises a gesture nobody is going to make with a mouse.

   So above the touch breakpoint a `.sheet` presents as a centred dialog. The
   markup does not change and neither does any behavior: the backdrop still
   dismisses, Escape still closes, focus is still managed. Only the shape moves.

   This block is what makes "a tray on touch, a dialog on a pointer device" true
   for the menu, genre and server overlays. Without it they render as bottom
   sheets at every width, which is exactly what a desktop user reported.
   -------------------------------------------------------------------------- */

@media (min-width: 768px) {
    .sheet {
        align-items: center;
        padding: 16px;
    }

    .sheet__panel {
        width: min(560px, 100%);
        max-height: 80vh;
        border-bottom: 1px solid var(--border);
        border-radius: var(--radius-lg);
        box-shadow: var(--elev-4);
    }

    /* The handle is a drag affordance and there is no drag here. */
    .sheet__grip {
        display: none;
    }

    /* Head padding did the spacing below the handle; without the handle the
       title needs the dialog's own top padding instead.

       Keyed on a preceding grip, and both heads are named, so the compensation
       lands on exactly the overlays that just lost a handle. It used to name
       `.sheet__head` alone, which left the detail and roulette dialogs — whose
       handles this same block hides — without it.

       `~` and not `+`: `.modal-backdrop-art` sits between the detail overlay's
       grip and its head, so the adjacent combinator would miss the one overlay
       most in need of matching. It does mean the rule depends on the grip
       staying a SIBLING of the head; nest it and this drops out silently, and
       the title simply sits 4px high.

       It must also stay inside this media query. A sibling selector matches a
       grip that is `display: none`, so the selector alone says "has a grip in
       the markup" — the breakpoint supplies "and it is hidden". That is the
       same breakpoint hiding it, immediately above, and the two must not drift
       apart.

       The trailer used to be named here as permanently exempt — it had no grip
       at any width, so it had no handle to compensate for. It has one now, and
       takes this compensation with every other overlay. The rule needed no
       edit for that, which is the point of keying it on the markup rather than
       on a list of overlay names. */
    .sheet__grip ~ .sheet__head,
    .sheet__grip ~ .modal__head {
        padding-top: 18px;
    }

    /* Scale like a dialog rather than sliding up from an edge it is no longer
       docked to. These override the tray rules above, so they must come after. */
    .overlay-opening .sheet__panel {
        transition: transform var(--dur-base) var(--ease-entrance);
    }

    .overlay-shut .sheet__panel {
        transform: scale(0.96);
    }

    .overlay-shown .sheet__panel {
        transform: scale(1);
    }
}

/* --------------------------------------------------------------------------
   Touch presentation

   Below this width a dialog that opted in becomes a tray: docked to the bottom
   edge, full width, square-shouldered at the bottom, with its grab handle back.
   Opting in is `.modal--tray-on-touch`.

   Every dialog in the app opts in, so the modifier now records that a panel
   carries the regions a tray needs rather than that it was chosen for the
   shape. It is kept as an explicit opt-in for exactly that reason: the modifier
   HIDES the × below this width, so applying it to a panel with no grab handle
   would produce an overlay with no visible way out. The modifier and the grip
   arrive together or not at all.

   The comment here used to name the trailer and the roulette as content-sized
   overlays that stay centred. The roulette had already opted in; the trailer
   has now. A comment listing exceptions is a registry by another route, and it
   went stale the same way one does. */

@media (max-width: 767px) {
    .modal--tray-on-touch {
        align-items: flex-end;
        padding: 0;
    }

    .modal--tray-on-touch .modal__panel {
        width: 100%;
        max-width: none;
        max-height: 88vh;
        border-bottom: none;
        border-radius: var(--radius-xl) var(--radius-xl) 0 0;
        box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.36), 0 -12px 32px rgba(0, 0, 0, 0.34);
    }

    /* The handle is the dismissal affordance here, so the × goes: two ways to
       close, one of which is a small target in the corner furthest from a
       thumb, is worse than one that is obvious. */
    .modal--tray-on-touch .sheet__grip {
        display: flex;
    }

    /* Both shapes, not just the dialog that opted in. A `.sheet` below this
       width IS a tray — that is what the pointer block above says by only
       turning it into a dialog above 768px — so the reasoning applies to it
       unchanged. It was scoped to `.modal--tray-on-touch` alone, which left the
       genre and server trays showing a handle AND a ×.

       This rule and the one above are a PAIR with the pointer block's
       `.sheet__grip { display: none }`. One hides the handle and shows the ×,
       the other does the reverse, and they must stay at the same breakpoint or
       a width exists where an overlay has neither affordance. That is not
       hypothetical here: the hand-off that hides the header's controls and
       shows the tray trigger drifted to 992px and 768px once already, leaving
       every width between with neither. */
    .sheet .overlay__close,
    .modal--tray-on-touch .overlay__close {
        display: none;
    }

    /* Slide rather than scale, matching a real tray. The panel rules above are
       keyed on .modal__panel, so these have to win on specificity.

       NO SPACE before `.modal--tray-on-touch`. Alpine puts the transition class
       on the overlay ROOT, which is the same element that carries the modifier —
       `<div class="modal modal--tray-on-touch overlay-shut">`. A descendant
       combinator therefore asks for an `.overlay-shut` ANCESTOR of the modifier,
       which does not exist, and all three of these matched **nothing** from the
       day they were written.

       Nothing errored and nothing looked broken on a desktop, because the
       generic `.overlay-shut .modal__panel { transform: scale(0.96) }` above is
       a legitimate rule that then wins by default. So the detail overlay and the
       roulette scaled like centred dialogs on a phone while every other tray
       slid, and the symptom read as "the tray animation feels wrong" rather than
       as a selector that never matched.

       Measured: `.overlay-shut .modal--tray-on-touch .modal__panel` matched 0
       elements; the compound form matches 1. */
    .overlay-opening.modal--tray-on-touch .modal__panel {
        transition: transform var(--dur-slow) var(--ease-entrance);
    }

    .overlay-shut.modal--tray-on-touch .modal__panel {
        transform: translateY(100%) scale(1);
    }

    .overlay-shown.modal--tray-on-touch .modal__panel {
        transform: translateY(0) scale(1);
    }
}
