/* ==========================================================================
   Overlay chrome — the surface, rows and list layer of an overlay.

   Controller: js/components/overlay.js (the box) and js/components/pop-list.js
   (the search and the cursor). This file owns how an overlay LOOKS, and the one
   piece of geometry that belongs to a thing this file invents: `.pop-menu`, the
   rows container, states its own padding, cap and scroll region here because
   nothing else in the app knows what a list of rows is. Where the BOX is and
   what the box scrolls are css/components/overlay.css, which loads after this
   one.

   Three classes make a menu: `.pop` is the surface, `.pop-menu` holds rows,
   `.pop-item` is a row.

   The surface comes from the shared floating-menu tokens (--menu-bg /
   --menu-border / --menu-shadow, in tokens.css) so menus, select2, the
   destination picker and the fetched panel all read as the same elevated
   surface. Nothing here may hardcode a background or a border.

   The filename says "menu" and the file is wider than that — every overlay in
   the app is dressed from here. It keeps the name because a rename is a
   {% static %} reference this build fails silently on, for no gain.
   ========================================================================== */

/* --- The surface --------------------------------------------------------- */

/* There is ONE surface and it is `.pop`, added by overlay.js on first open. A
   body therefore says `popover` and nothing else — no class at all — and a menu,
   a fetched form and a client-built confirm all read as the same card.

   `[data-pop-bare]` on the body opts out, for the one shape that already paints
   its own: the destination picker's `.upoly-panel`, which is flush against its
   trigger and shares the trigger's border. */
.pop:not([data-pop-bare]) {
    color: var(--text-primary);
    background: var(--menu-bg);
    border: 1px solid var(--menu-border);
    border-radius: 8px;
    box-shadow: var(--menu-shadow);
    /* The UA stylesheet gives every `[popover]` `padding: 0.25em`, and this is
       what takes it off. The box pads nothing: what a menu is inset by is
       `.pop-menu`'s, wherever that class sits, and a fetched form that brings its
       own spacing never sits inside a second inset. */
    padding: 0;
}

/* Geometry the surface does not decide, so it applies to a bare body too.
 *
 * A floor, not a width: `data-pop-width="anchor"` on the trigger is how a body
 * asks to be as wide as the thing it drops from, and it is a full declaration
 * rather than a `min-width` because a form field's dropdown must MATCH its field.
 *
 * The gap from the trigger is `offset()` in overlay.js, measured in the same pass
 * as the position, so no margin belongs here; `.pop` in overlay.css already
 * states `margin: 0`, which is also what takes the UA's margin off a <ul> body. */
.pop {
    /* The spacing scale, in one place. `--pop-inset` is the gap from the box's edge to
       anything inside it; `--pop-gap` is the gap between two of those things; and every
       bar and row pads its own text by `--pop-pad-x`, so a search icon, a head title, a
       row's label and a foot's button all start on one vertical line. */
    --pop-inset: 6px;
    --pop-gap: 2px;
    --pop-pad-x: 0.75rem;

    min-width: 10rem;
    /* A menu is sometimes a <ul>. */
    list-style: none;
}

/* Stated twice over: this is the box-level menu, the rule below is the child.
   Both read the one scale so they cannot drift apart. */
.pop.pop-menu {
    padding: var(--pop-inset);
}

/* --- Trigger ------------------------------------------------------------- */

/* Every trigger is clickable, including the ones that are an <a> with no href
   (the topbar's wrench) or a <span> (a status badge). Stated once here rather
   than as an inline `cursor: pointer` on each. */
[data-pop] {
    cursor: pointer;
}

/* Open.
 *
 * `aria-expanded` is already on the trigger — the controller sets it true on open and
 * false on close — so the state needs no new hook, no class and no JS. It also means
 * the style cannot outlive the overlay: the attribute flips in the same breath the box
 * starts closing.
 *
 * A ring rather than a background or a border. A trigger here is a `.btn`, a `.badge`,
 * a bare `<span>` or a topbar `<a>`, and a ring is the only affordance that reads the
 * same on all four — a background fights the badge's own colour, and a border changes
 * the box's size and shifts everything beside it.
 *
 * Drawn from `currentColor`, for the reason the caret below is: a fixed grey disappears
 * on a coloured badge and reads too heavy on a muted one, while currentColor tracks
 * whatever the trigger already is.
 *
 * `!important` is not decoration here. `layout.css` carries a blanket
 * `*:focus { outline: 0 !important; box-shadow: none !important }`, and an open trigger
 * is nearly always also the focused element, so anything weaker loses to it. That reset
 * is also why closing needs no work: with focus rings suppressed app-wide, a trigger
 * that keeps DOM focus after its overlay closes shows nothing. Focus itself is left
 * where `closed()` puts it, so the keyboard can still reopen from the same spot. */
[data-pop][aria-expanded="true"] {
    box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 20%, transparent) !important;
}

/* Ghost triggers opt out of the ring.
 *
 * A ghost button is transparent at rest, so a halo drawn around it hangs in space with
 * no box to hug. It already owns a raised look — the hover fill — so open reuses it:
 * the surface stays lit and the label stays at full strength for as long as the overlay
 * is up, which reads as the button being held down rather than outlined.
 *
 * `!important` on the shadow only to beat the ring above, which needs its own for
 * layout.css's blanket focus reset. */
.btn.btn-ghost[data-pop][aria-expanded="true"] {
    box-shadow: none !important;
    background: var(--surface-3);
    color: var(--text-primary);
}

/* Same for the destructive ghost, in its own colour — it outranks the rule above, which
 * would otherwise paint an open danger trigger neutral grey even under the cursor. */
.btn.btn-ghost-danger[data-pop][aria-expanded="true"] {
    background: rgba(var(--bs-danger-rgb), 0.12);
    color: var(--bs-danger);
}

/* The caret.
 *
 * A border triangle, not a glyph: `fa-caret-down` is a whole text glyph and reads
 * about twice the size beside a badge.
 *
 * Sized in PIXELS, because `em` resolves against whatever font-size the trigger's
 * context happens to impose on an <i> — and `layout.css` gives every <i> in a topbar
 * nav link `font-size: 17px` for Font Awesome's benefit, at a specificity nothing in
 * this file can reach. An `em` caret comes out a fifth larger in the topbar than on a
 * badge; an absolute size is immune and makes every caret in the app identical.
 *
 * MUTED. `border-top: solid` with no colour inherits `currentColor`, which on a bright
 * badge or nav link paints the caret at full strength and reads far too heavy.
 * currentColor at reduced opacity dims it while still tracking whatever colour the
 * trigger is, which a fixed grey cannot do on a coloured badge.
 *
 * An element, not a pseudo-element: a trigger carrying `hx-get` wears `.htmx-request`
 * while it loads, and the scanner indicator draws itself with `::after` (state.css),
 * which would take the caret away for the whole request. */
.pop-caret {
    --pop-caret-size: 4px;
    display: inline-block;
    /* The spinner below is drawn from this box. */
    position: relative;
    /* Never a flex item that stretches or shrinks: most triggers here are
       `inline-flex` (.badge, .btn, the topbar's .d-flex nav links). */
    flex: none;
    width: 0;
    height: 0;
    margin-left: 3px;
    vertical-align: middle;
    border-top: var(--pop-caret-size) solid;
    border-right: var(--pop-caret-size) solid transparent;
    border-bottom: 0;
    border-left: var(--pop-caret-size) solid transparent;
    opacity: .55;
}

/* --- The wait ------------------------------------------------------------ */

/* A fetched body is held back until its content has arrived, so that the overlay
 * appears once, at its final size (overlay.js). The cost of that is a gap between
 * the click and anything happening — on the domain switcher, long enough that the
 * trigger reads as a control that did not work.
 *
 * `data-pop-busy` is on the TRIGGER for that gap, written and dropped by overlay.js.
 *
 * Every rule below is delayed by --pop-busy-wait and every one is an ANIMATION
 * rather than a transition. The delay is the point: most of these responses come
 * back inside 200ms, and an indicator that appears and leaves within two frames
 * reads as a flicker fault rather than as progress. A transition cannot express
 * that — its delay would apply in both directions, holding the indicator on the
 * screen after the wait was over rather than off it before the wait had begun.
 *
 * The other end of the same problem — a response that lands just AFTER the delay
 * expires, drawing the indicator for two frames instead of not at all — is
 * overlay.js's: once drawn, the attribute is held for a minimum. Nothing here has
 * to know, which is why the timing lives in one place and not two. */
/* On the root, not on the trigger, so it can be overridden from anywhere — and so
   `--pop-busy-wait: 0ms` in the console shows you the state on every request,
   however fast, which is the only way to see one that is behaving. */
:root {
    --pop-busy-wait: 120ms;
}

[data-pop-busy] {
    cursor: progress;
}

/* The caret becomes the spinner, in place. The ring is absolutely positioned from
 * the caret's own box, so nothing around it moves — no topbar reflow, no badge
 * growing a pixel while it loads — and a trigger already wearing an htmx indicator
 * on its own ::after is untouched, because this one belongs to the caret. */
[data-pop-busy] .pop-caret {
    animation: pop-busy-caret 120ms linear var(--pop-busy-wait) forwards;
}

.pop-caret::after {
    content: "";
    position: absolute;
    /* The caret's padding box is 0x0 and sits at the triangle's tip: horizontally
       centred in the border box, 4px down from its top. Hence the offset up. */
    top: -2px;
    left: 0;
    width: 9px;
    height: 9px;
    translate: -50% -50%;
    border: 1.5px solid currentColor;
    /* The gap that makes the rotation legible — a full ring spinning looks static. */
    border-top-color: transparent;
    border-radius: 50%;
    opacity: 0;
}

[data-pop-busy] .pop-caret::after {
    animation: pop-busy-in 120ms linear var(--pop-busy-wait) forwards,
    pop-busy-spin 600ms linear var(--pop-busy-wait) infinite;
}

/* An icon-only trigger — the tags button — has no caret to swap, so the control
   itself backs off. Same window, same delay. */
[data-pop-busy]:not(:has(.pop-caret)) {
    animation: pop-busy-dim 120ms linear var(--pop-busy-wait) forwards;
}

@keyframes pop-busy-caret {
    to {
        border-top-color: transparent;
    }
}

@keyframes pop-busy-in {
    to {
        opacity: .9;
    }
}

@keyframes pop-busy-dim {
    to {
        opacity: .45;
    }
}

@keyframes pop-busy-spin {
    to {
        rotate: 360deg;
    }
}

/* The ring stays — it is the only thing saying the click landed — but it holds
   still instead of spinning. */
@media (prefers-reduced-motion: reduce) {
    [data-pop-busy] .pop-caret::after {
        animation: pop-busy-in 1ms linear var(--pop-busy-wait) forwards;
    }
}

/* --- Rows ---------------------------------------------------------------- */

/* Rows are pills: rounded corners so any fill floats inside the panel instead of
   running into its border. The gutter around them is the container's
   `--pop-inset`, on all four sides — the row itself spaces nothing, so a row can
   never be inset differently from the search bar or the foot above and below it.
 *
 * Full width, so a trailing `ml-auto` badge or tick has slack to push into. A
 * <button> shrinks to fit otherwise; `100%` is the container's content box, which
 * is already inside the inset, so it fills the gutter exactly and adds nothing on
 * either side. */
.pop-item {
    display: flex;
    align-items: center;
    width: 100%;
    margin: 0;
    padding: 0.5rem var(--pop-pad-x);
    border: 0;
    border-radius: 6px;
    background: transparent;
    color: var(--text-secondary);
    font-size: 13px;
    text-align: left;
    cursor: pointer;
}

.pop-item:hover,
.pop-item:focus {
    background: rgba(var(--bs-primary-rgb), 0.1);
    color: var(--text-primary);
    text-decoration: none;
}

.pop-item:active {
    background: rgba(var(--bs-primary-rgb), 0.15);
    color: var(--text-primary);
}

/* The keyboard cursor (pop-list.js). Deliberately the same weight as :hover —
   it marks what Enter will pick, and having it read differently from the row
   under the pointer is what makes a list feel like it has two selections. */
.pop-item.is-active {
    background: rgba(var(--bs-primary-rgb), 0.1);
    color: var(--text-primary);
}

.pop-item.disabled,
.pop-item:disabled {
    opacity: .5;
    pointer-events: none;
}

/* --- Pinned rows ---------------------------------------------------------- */

/* A row, or a few, held above the list: "All locations", "Any status", a create
   action. It sits OUTSIDE `.pop-menu`, which is what makes it pinned — the rows
   container caps itself so the box never scrolls — and also means the search never
   filters it away and the keyboard cursor never opens on it.
 *
 * Its border IS the list's top edge, so it carries no margin. A `.pop-divider` here
 * looks the same standing still and is wrong the moment the list scrolls: its
 * half-rem margins leave the scroll shadow floating 8px clear of the line it is meant
 * to continue, which reads as an artefact rather than as depth. Same reasoning, and
 * the same border, as `.pop-search` above.
 *
 * Full-bleed, unlike the rows it holds. A row is inset `0 6px` so its hover pill has
 * somewhere to sit; a rule that separates two regions of the menu spans the menu. */
/* Holds rows, so it insets and spaces them exactly as `.pop-menu` does — a pinned row
   and a scrolling one are the same row and must line up. */
.pop-pinned {
    display: flex;
    flex-direction: column;
    gap: var(--pop-gap);
    padding: var(--pop-inset);
    border-bottom: 1px solid var(--border-subtle);
}

/* --- Multi-select rows ---------------------------------------------------- */

/* A row that holds a checkbox. The checkbox IS the state — it is what posts, and what
   `pop-list.js` mirrors onto `.is-sel` — so the row shows that state its own way and
   the input itself is taken out of the picture.
 *
 * `position: relative` on the row is not optional: it makes the row the containing
 * block for its own absolute checkbox. Without it every checkbox anchors to the menu
 * and they lay out down the full unclipped list height, escaping the scroll region.
 *
 * The `:has()` here is row-scoped — it asks what THIS row contains, not whether some
 * descendant of a box exists — so it cannot repeat the ancestor-matching trap that
 * asking a box whether it contains a list was: that matched every ANCESTOR of one,
 * and handed a form holding a picker a layout it never asked for. */
.pop-item:has(> input[type="checkbox"]) {
    position: relative;
}

.pop-item > input[type="checkbox"] {
    position: absolute;
    opacity: 0;
    pointer-events: none;
}

/* An optional trailing tick, for rows that want one. Driven by the same `.is-sel`
   pop-list.js writes, so a template never wires visibility itself. */
.pop-check {
    margin-left: auto;
    padding-left: .75rem;
    visibility: hidden;
}

.pop-item.is-sel .pop-check {
    visibility: visible;
}

/* The current value. One hue and one ladder for the whole menu:
 *
 *     hover, focus, keyboard cursor            .10
 *     pressed                                  .15
 *     picked, at rest                          .14   + tick
 *     picked, hovered or under the cursor      .22   + tick
 *
 * A fill, not a pill. This used to be a `.btn-primary` in row form — gradient, inset
 * edge, `--btn-fg-on-fill` label, heavier weight — which made a picked row a different
 * KIND of thing from the row under the cursor rather than the same row in another
 * state, and the two read as two separate systems in one list. It was also unusable
 * for multi-select, where thirty of them at once is a wall of colour.
 *
 * What says "picked" is the tick. What says "you are here" is the tint. Neither has to
 * shout, because they are never asking the same question. */
.pop-item.is-sel {
    background: rgba(var(--bs-primary-rgb), 0.14);
    color: var(--text-primary);
}

.pop-item.is-sel:hover,
.pop-item.is-sel:focus,
.pop-item.is-sel.is-active {
    background: rgba(var(--bs-primary-rgb), 0.22);
    color: var(--text-primary);
}

/* Opt-out for menus where most rows are picked at once (e.g. the column toggle,
   where every column starts checked): the tint would flood the list, and the
   checkbox glyph already says "picked". Selected rows sit at rest like any other
   row and keep only the ordinary hover/cursor tint. */
.pop-menu.pop-quiet-sel .pop-item.is-sel {
    background: transparent;
    color: var(--text-secondary);
}

.pop-menu.pop-quiet-sel .pop-item.is-sel:hover,
.pop-menu.pop-quiet-sel .pop-item.is-sel:focus,
.pop-menu.pop-quiet-sel .pop-item.is-sel.is-active {
    background: rgba(var(--bs-primary-rgb), 0.1);
    color: var(--text-primary);
}

/* A destructive row keeps its cue through hover, where the rule above would
   otherwise force it to --text-primary. */
.pop-item.text-danger,
.pop-item.text-danger:hover,
.pop-item.text-danger:focus,
.pop-item.text-danger.is-active {
    color: var(--bs-danger) !important;
}

.pop-item.text-danger:hover,
.pop-item.text-danger:focus,
.pop-item.text-danger.is-active {
    background: rgba(var(--bs-danger-rgb), 0.12);
}

/* Secondary text on a row — an email beside a name, an extension beside a
   label. Pushed right by `ml-auto` in the markup where that is wanted. */
/* The row's main label. Truncates rather than wraps, so a long value cannot push the
   trailing chrome — a tick, a secondary value, a count — off the row or grow the row
   to two lines. `min-width: 0` because a flex item refuses to shrink below its content
   without it, which is what makes `text-overflow` silently do nothing. */
.pop-item-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text-primary);
}

.pop-item-meta {
    font-size: 12px;
    color: var(--text-muted);
}

/* --- Section furniture --------------------------------------------------- */

/* A small uppercase caption INSIDE a list. Named `label` rather than `header`
   because `.pop-head` above it is the pinned bar, and two classes one letter
   apart that mean different things is a bug waiting to be typed. */
.pop-label {
    color: var(--text-tertiary);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-weight: 600;
    /* The same horizontal padding a row has, so a heading and the rows under it start
       their text on one vertical line. */
    padding: 0.5rem var(--pop-pad-x);
}

/* A rule between two groups of rows. It gets more air than the `gap` between two rows
   — that is the whole point of drawing it — and the air is `--pop-inset`, the same
   number the box uses at its edges, so a menu has exactly two vertical distances in it:
   rows within a group sit `--pop-gap` apart, and a group stands `--pop-inset` clear of
   the next one and of the box's own edge.
 *
 * The `gap` already contributes `--pop-gap` on each side, so the margin makes up the
 * difference. It is symmetric by construction, and it is the one margin in the file for
 * the one thing whose spacing is not the container's to decide. */
.pop-divider {
    height: 0;
    margin-block: calc(var(--pop-inset) - var(--pop-gap));
    border-top: 1px solid var(--border-normal);
}

/* --- Pinned bars ---------------------------------------------------------- */

/* A bar that stays while the rows move — which it does by simply not being inside the
   part that scrolls. `.pop-menu` caps itself, so the box never scrolls, so anything
   outside it is already pinned and no measuring, flex column or sticky offset is
   involved.

   `sticky` and the opaque background remain for the one shape that scrolls itself: a
   menu whose rows are its direct children, where `.pop-menu` IS the box. */
.pop-head,
.pop-foot,
.pop-search {
    position: sticky;
    z-index: 1;
    background: var(--pop-bg);
}

.pop-head,
.pop-search {
    top: 0;
}

.pop-foot {
    bottom: 0;
    padding: 9px calc(var(--pop-inset) + var(--pop-pad-x));
    border-top: 1px solid var(--border-subtle);
}

/* A bar holding ROWS rather than text of its own — a pinned "Add new …" under a list
   of choices, a header holding a row.
 *
 * It insets exactly as `.pop-menu` does, because its rows ARE the menu's rows and have
 * to line up with them on both edges. Padding it like a bar instead puts a row's text
 * three insets deep — the bar's, plus the row's own — and stops its pill an inset and
 * a half short of the right edge, which is the bug that reads as "too much margin".
 *
 * `var(--pop-inset)`, never a literal: a copy of the number drifts the moment the
 * scale moves, and then only the rows in bars are wrong. */
.pop-head--rows,
.pop-foot--rows {
    display: flex;
    flex-direction: column;
    gap: var(--pop-gap);
    padding: var(--pop-inset);
}

/* A title and a close X, laid out with utilities in the markup; this owns the
   bar's padding and border and the X's red hover. */
.pop-head {
    padding: 11px calc(var(--pop-inset) + var(--pop-pad-x));
    border-bottom: 1px solid var(--border-subtle);
}

.pop-head .ttl {
    font-size: 12px;
    letter-spacing: .06em;
}

.pop-head .x:hover,
.pop-head .x:focus {
    color: var(--color-red) !important;
    background: rgba(239, 68, 68, .12);
    text-decoration: none !important;
}

/* --- The list layer ------------------------------------------------------ */

/* A search field pinned above the rows. Sticky (above), not a flex sibling — the box
   scrolls and the field stays. It must also stay OUT of any scroll region of its own:
   a caret inside a scroller drags that scroller back into view every time the overlay
   is re-measured, which reads as a scrollbar that will not move. */
.pop-search {
    display: flex;
    align-items: center;
    gap: 8px;
    /* 18px each side puts the magnifier on the same vertical line as the icon in
       every row below it — a row is inset 6px and padded 12px. The head and foot
       sit at 14px because they are blocks of their own, but the search field reads
       as the first thing in the list and lining it up with the list says so. */
    padding: 9px calc(var(--pop-inset) + var(--pop-pad-x)) 10px;
    color: var(--text-muted);
    border-bottom: 1px solid var(--border-subtle);
    /* No bottom margin. The scroll shadow starts at the top edge of the list, so any
       gap here is a gap between the border and the shadow — and a shadow floating
       four pixels clear of the line it is meant to continue reads as an artefact
       rather than as depth. */
}

.pop-search input {
    flex: 1 1 auto;
    min-width: 0;
    background: transparent;
    border: none;
    outline: none;
    color: var(--text-primary);
    font-size: 13px;
    /* The caret is the brightest thing in the panel by default — full --text-primary,
       blinking, on a dim surface — and it pulls the eye off the rows, which are what
       the reader is actually scanning. Dimmer, and it still says where typing lands. */
    caret-color: var(--text-muted);
}

/* A keyboard-shortcut chip in the search row. `forms.css` positions `.input-kbd-hint`
   absolutely against a relative field wrapper; a `.pop-search` is a flex line instead,
   so the chip rides in it. The `transform` has to be undone with the positioning — the
   shared rule pairs `top: 50%` with `translateY(-50%)` to centre an absolute chip, and
   the transform still applies once the element is static, shifting it half its own
   height upwards. */
.pop-search .input-kbd-hint {
    flex: none;
    position: static;
    transform: none;
}

.pop-search input::placeholder {
    color: var(--text-muted);
}

/* The rows container.
 *
 * `.pop-menu` says "this element holds menu rows", and it may be EITHER place: the
 * box itself, when the rows are its direct children, or a child of the box, when a
 * search field or a pinned bar has to stay put while the rows scroll. One class for
 * both shapes is what makes a row stand off the box's rounded corner by the same 4px
 * either way — the whole point of naming the container at all.
 *
 * The cap is PAIRED with the room the `size` middleware found, and the pairing is not
 * optional: capped at 30rem alone, a menu opened near the bottom of the screen gets a
 * short box that scrolls AND a 30rem rows container scrolling inside it — two
 * scrollbars for one list. A length rather than a row count, because rows are not all
 * one height.
 *
 * `:not(.is-sheet)` because a sheet is sized against the screen it lies on
 * (overlay.css) and `--pop-avail-height` is not maintained while an overlay is one.
 *
 * The scroll region is here rather than on the box because the box may hold bars the
 * rows must scroll under: a bar is pinned by sitting OUTSIDE this element, and no
 * measuring, flex column or `:has()` is involved. When the class is on the box there
 * is nothing outside it and the box is simply its own scroll region. */
/* One spacing system, and only two numbers in it: a container insets its edges by
   `--pop-inset`, and a `gap` of `--pop-gap` separates whatever it holds. Nothing
   inside carries a width of its own, and the only margin in the file is the
   `.pop-divider`'s, which buys back the difference between the two numbers.
 *
 * `gap` rather than margins on the rows, because a margin has to be right at both the
 * ends and between siblings and cannot be — the ends then read as inset + margin while
 * the middles read as one collapsed margin, which is what left dividers with 8px of air
 * against the rows' 2px and rows sitting 8px from the top edge but 6px from the side. A
 * gap applies between every pair and never at an end, so the inset is exactly the
 * padding, everywhere, for rows and dividers and labels alike.
 *
 * `.pop-pinned` and `.pop-foot--rows` state the same three declarations, because they
 * hold the same rows and a pinned row must line up with a scrolling one. */
.pop-menu {
    display: flex;
    flex-direction: column;
    gap: var(--pop-gap);
    padding: var(--pop-inset);
    overflow: hidden auto;
    overscroll-behavior: contain;
    /* Keeps the cursor row off the hard scroll edge as the arrows walk it, so the row
       Enter would pick is never the half-clipped one against the rim. */
    scroll-padding-block: 0.75rem;
}

.pop-menu:not(.is-sheet) {
    max-height: min(var(--pop-menu-max, 30rem), var(--pop-avail-height, 100dvh));
}

/* Scroll shadows, on the rows container — the element that scrolls.

   The shadows are four backgrounds and no JS. Each edge gets an opaque COVER in the
   panel's own colour, attached `local` so it travels with the content, and a shadow
   attached `scroll` so it stays pinned to the edge. At rest the cover sits exactly
   over the shadow and hides it; scroll, and the cover slides away to reveal it. So
   the cue is not "this box scrolls" but "there is more content in this direction",
   which is the question a reader actually has — and a short list that cannot scroll
   never shows one.

   Linear, not radial. `radial-gradient(farthest-side at 50% 0)` is the usual recipe
   and it is wrong here: it peaks in the middle and falls to nothing at the left and
   right edges, so the shadow is weakest exactly where it should be continuing the
   border above it, and the whole thing reads as a smudge. Full width, even weight —
   which also means it needs FAR less alpha than the radial did to be legible.

   Each cover is opaque for its first half before it fades, and is taller than the
   shadow it hides, so nothing bleeds through at rest.

   Listed covers first because the first background in the list paints on top. */
.pop-menu {
    /* The two numbers worth touching. The shadow has to darken the sliver directly
       under the border and stop — go much past this and it stops reading as depth
       and starts reading as a band laid over the first row's text. */
    --pop-scroll-shadow: rgba(0, 0, 0, .28);
    --pop-scroll-shadow-size: 9px;

    /* Longhands, never the `background` shorthand: the shorthand resets
       `background-color`, and each box's surface is its own — `--menu-bg` for a menu,
       `--pop-bg` for a panel. Writing it here took that surface away and left the
       covers doing the job of the whole background. */
    background-image:
        linear-gradient(var(--pop-bg) 55%, rgba(0, 0, 0, 0)),
        linear-gradient(rgba(0, 0, 0, 0), var(--pop-bg) 45%),
        linear-gradient(var(--pop-scroll-shadow), rgba(0, 0, 0, 0)),
        linear-gradient(rgba(0, 0, 0, 0), var(--pop-scroll-shadow));
    background-position: top, bottom, top, bottom;
    background-size:
        100% 20px, 100% 20px,
        100% var(--pop-scroll-shadow-size), 100% var(--pop-scroll-shadow-size);
    background-repeat: no-repeat;
    background-attachment: local, local, scroll, scroll;
}

.pop-empty {
    padding: 0.75rem var(--pop-pad-x);
    color: var(--text-muted);
    font-size: 13px;
    text-align: center;
}

/* --- Scrollbars inside an overlay ---------------------------------------- */

.pop ::-webkit-scrollbar {
    width: 8px;
}

.pop ::-webkit-scrollbar-track {
    background: var(--scrollbar-track);
    border-radius: 4px;
}

.pop ::-webkit-scrollbar-thumb {
    background: var(--scrollbar-thumb);
    border-radius: 4px;
}

.pop ::-webkit-scrollbar-thumb:hover {
    background: var(--scrollbar-thumb-hover);
}

.pop * {
    scrollbar-width: thin;
    scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
