@layer components {
  /* Comboboxes — a text input with a list of options anchored under it.
   *
   * The multi-select is the same component with chips in the field, which is
   * why there isn't a second one: "choose one" and "choose several" differ in
   * how the answer is shown, not in what the thing is.
   *
   * It composes with .popover rather than restating it. The wrapper carries
   * both classes, so the anchoring, the flipping, the fade and every dismissal
   * are the ones already in the app, and this file adds only what a combobox
   * has that a popover doesn't: a field that looks like an input, a list of
   * options, and a tick against the ones that are chosen. .popover is
   * inline-flex, so the label and the field stack by setting flex-direction
   * rather than by fighting over `display`.
   */
  .combobox {
    --combobox-width: 18rem;

    flex-direction: column;
    gap: var(--space-quarter);
    inline-size: var(--combobox-width);
    max-inline-size: 100%;
  }

  .combobox--wide {
    --combobox-width: 26rem;
  }

  /* Fills whatever is holding it, for a column of fields where a select has to
     end on the same line as the text inputs above and below it. The default
     narrow width beside a full-width input is the misalignment a settings form is
     nothing but — see .channel-settings. A variant rather than a --combobox-width
     set from the consuming block, which lint rule 4 exists to stop. */
  .combobox--block {
    --combobox-width: 100%;
  }

  /* For a row of them, where each one is a filter rather than a field: narrow
     enough that four fit across, and the chips inside say what is set. */
  .combobox--compact {
    --combobox-width: 11rem;
  }

  .combobox__label {
    color: var(--color-ink-soft);
    font-size: var(--text-small);
    font-weight: var(--weight-medium);
  }

  /* The field is what reads as the input: the border, the radius and the focus
     ring are here, and the real <input> inside is bare. That is what lets chips
     sit in the box beside the caret instead of above or below it.
   *
   * It is also what the surface hangs off — position: relative here rather than
   * on .combobox, so a flipped combobox opens against the top of the field
   * instead of above the label, leaving a gap the width of the label behind it.
   */
  .combobox__field {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-quarter);
    padding: var(--field-padding-block) var(--field-padding-inline);
    border: var(--border);
    border-radius: var(--radius);
    background: var(--color-canvas);
    cursor: text;
  }

  /* Its focus ring is in forms.css, with the other controls whose wrapper draws
     the border — including the rule that stops the input drawing a second one
     inside it. */

  /* Scoped through the field on purpose: forms.css styles every input inside a
     .form__field, and a combobox dropped into one would otherwise grow a second
     border inside its own. */
  .combobox__field .combobox__input {
    --combobox-input-min: 4rem;

    flex: 1;
    min-inline-size: var(--combobox-input-min);
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
  }

  /* A select — a combobox that takes no typing. Its field is something you
     press rather than something you type in, and the caret would say otherwise. */
  .combobox__field:has([readonly]) {
    cursor: pointer;

    .combobox__input {
      cursor: pointer;
      user-select: none;
    }
  }

  /* Not a tab stop. The input opens the list with ArrowDown and closes it with
     Escape, so a second stop for the same job is one more thing to tab past;
     a pointer or a thumb still reaches it. */
  .combobox__toggle {
    --combobox-toggle-size: var(--control-size-small);

    display: flex;
    align-items: center;
    justify-content: center;
    inline-size: var(--combobox-toggle-size);
    block-size: var(--combobox-toggle-size);
    margin-inline-start: auto;
    padding: 0;
    border: 0;
    border-radius: var(--radius-circle);
    background: none;
    color: var(--color-ink-soft);
    font-size: var(--text-xx-small);
    cursor: pointer;
    transition: rotate var(--transition-fast);

    @media (any-hover: hover) {
      &:hover {
        background: var(--color-highlight);
        color: var(--color-ink);
      }
    }
  }

  .combobox:has(dialog[open]) .combobox__toggle {
    rotate: 180deg;
  }

  /* As wide as the field, which a popover's max-content sizing is not. The
     height is the surface's rather than the list's, so the footer can stick to
     the bottom of it while the options scroll underneath.
   *
   * Scoped through the field to outrank .popover__surface, which is one class
   * in the same layer — a tie that source order would settle, and source order
   * here is the alphabet. Written flat, every one of these lost to popover.css
   * and the surface came out content-width against the popover's own minimum,
   * which is narrower than most fields and wider than some. */
  .combobox__field .combobox__surface {
    --combobox-surface-height: min(20rem, 60dvh);

    /* The field's measured width rather than 100%: the surface is fixed now, so
       a percentage would be of the viewport. anchoring_helpers writes it. */
    inline-size: var(--anchor-width);
    min-inline-size: 0;
    max-inline-size: none;
    max-block-size: var(--combobox-surface-height);
  }

  /* Ordinary block flow, and the gap is a margin on the options rather than a
     grid gap. That is not a preference: where the options come from the server
     they arrive inside Turbo Frames nested in this list, and a grid can only
     space its own children — the second page's options would be one grid item
     with no space between them, and the third page's another inside that. A
     margin belongs to the option and travels with it however deep it sits. */
  .combobox__list {
    margin: 0;
    padding: var(--space-half);
    list-style: none;
  }

  /* Where the options come from the server, they arrive inside a Turbo Frame
     that sits between the list and its rows — so the id aria-controls points at
     is on the page from the first paint rather than arriving with the first
     answer. Fizzy's pagination frames sit inside its lists the same way, marked
     presentational for the same reason.
   *
   * A block, and this is the line the whole of paging rests on. A custom
   * element is inline by default and `display: contents` looks tidier, but both
   * leave the frame without a box of its own — and a lazy frame loads when its
   * box scrolls into view. With no box there is nothing to observe: the next
   * page either never arrives or arrives immediately, depending on which
   * nothing the browser measures. */
  .combobox__list turbo-frame {
    display: block;
  }

  /* While a search is in flight, the list still holds the previous answer.
     Saying so beats swapping in placeholder rows on every keystroke: at typing
     speed that is a flicker, and the rows that flicker are the ones being read.
     Turbo marks a loading frame busy, and this is only that mark made visible.

     Matched on the list rather than on the frame, because a frame that draws no
     box has no opacity to take — and on the direct child, so the next page
     loading at the bottom doesn't dim the twenty rows above it. */
  .combobox__list:has(> turbo-frame[busy]) {
    opacity: var(--opacity-soft);
    transition: opacity var(--transition-fast);
  }

  /* The search box, for a list that comes from the server.
   *
   * A local combobox is typed at in its own field, which can hold the query
   * because the list narrows underneath it. A remote one cannot: the field has
   * to keep showing what is chosen while the options are replaced under it, and
   * a readonly field is also what keeps a phone's keyboard down until there is
   * somewhere to type. So the query goes above the list — and stays there while
   * the options scroll past, the same way the multi-select's footer stays at
   * the other end. */
  .combobox__search {
    position: sticky;
    inset-block-start: 0;
    z-index: var(--z-sticky-header);
    display: flex;
    align-items: center;
    gap: var(--space-half);
    padding: var(--space-half) var(--space-three-quarters);
    border-block-end: var(--border-faint);
    /* Opaque, or the options would read through it on the way past. */
    background: var(--color-canvas);
    color: var(--color-ink-soft);

    /* It is the top edge of the surface, so it takes the surface's own top
       corners. Without them the focus ring below drew a square-cornered
       rectangle inside a rounded box, and against a flush surface — the
       property switcher, where the box is focused the moment the popover
       opens — that read as a stray border rather than as focus.

       `inherit` rather than the token, because the surface it sits in decides
       its own radius: .popover--flush and .combobox__surface do not have to
       agree for this to follow whichever one it is in. Harmless where the
       surface has padding, since the strip is not against the corner there. */
    border-start-start-radius: inherit;
    border-start-end-radius: inherit;

    /* The strip is what reads as the field here, so the strip takes the ring —
       the same decision forms.css makes for the three wrappers that grow a
       border around an input. It is drawn inside its own edge rather than
       outside it, because this one runs the full width of the surface and a
       ring offset outward would be clipped by it. */
    &:focus-within {
      outline: var(--border-width-heavy) solid var(--color-accent);
      outline-offset: calc(var(--border-width-heavy) * -1);
    }
  }

  .combobox__search-input {
    flex: 1;
    min-inline-size: 0;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: var(--color-ink);

    &:focus-visible {
      outline: none;
    }
  }

  /* One highlight, not two. Pointing at an option makes it the active one
     through navigable-list, so the mouse and the arrow keys move the same
     marker and can't disagree about which option Enter would take. */
  .combobox__option {
    display: flex;
    align-items: center;
    gap: var(--control-gap);
    /* The space between options — see the note on .combobox__list for why it is
       here rather than a gap up there. Harmless on the first one: it is inside
       the list's own padding. */
    margin-block-start: var(--space-eighth);
    padding: var(--space-quarter) var(--space-half);
    border-radius: var(--radius-small);
    font-size: var(--text-normal);
    cursor: pointer;

    &[data-active] {
      background: var(--color-highlight);
    }

    &[aria-selected="true"] .combobox__check {
      visibility: visible;
    }
  }

  /* Hidden rather than absent, so the label doesn't shift sideways as options
     are ticked and unticked. */
  .combobox__check {
    color: var(--color-accent);
    visibility: hidden;
  }

  .combobox__hint {
    margin-inline-start: auto;
    padding-inline-start: var(--space-half);
    color: var(--color-ink-soft);
    font-size: var(--text-x-small);
  }

  .combobox__chip {
    --combobox-chip-width: 12rem;

    max-inline-size: var(--combobox-chip-width);

    /* The label truncates; the × does not, or a long value would push the way
       to get rid of it out of the chip. */
    > span {
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
  }

  .combobox__empty {
    margin: 0;
    padding: var(--space-half) var(--space-half) var(--space-and-quarter);
    color: var(--color-ink-soft);
    font-size: var(--text-normal);
    text-align: center;
  }

  /* .combobox__footer is in footer.css, with the other three. */
}
