/* Modal dialog. DESIGN.md section 4 (Drawer, popover, dialog, reference
   stack) and section 6 (radius-lg for dialogs, z-overlay budget). */

@layer components {
  .dialog {
    /* One place to state how tall a dialog may be. Before this token the cap
       was two separate literals - here and on the panel - so a dialog that
       wanted to be taller (the component editor) could raise one of them and
       silently keep the other's cap.

       THE PANEL NO LONGER REPEATS IT. It is a grid item of this box instead,
       and takes its height from layout - see `.dialog[open]` below. */
    --dialog-max-block: min(84dvh, 760px);
    padding: 0;
    border: 1px solid var(--border);
    border-radius: 0;
    background: var(--surface-3);
    backdrop-filter: var(--glass-filter-strong);
    color: var(--text);
    box-shadow: var(--shadow-2);
    width: min(560px, calc(100vw - var(--space-6)));
    max-height: var(--dialog-max-block);
    z-index: var(--z-overlay);
    opacity: 0;
    transform: scale(0.97) translateY(var(--space-2));
    transition: opacity var(--dur-3) var(--ease-out),
                transform var(--dur-3) var(--ease-spring);
  }

  /* THE DIALOG IS A GRID, AND THAT IS A BUG FIX (owner, 2026-09-24: "in a lot
     of component types its making 2 scrollbars on the right side").

     The panel used to carry `max-height: var(--dialog-max-block)` of its own,
     to give its `auto 1fr auto` rows something definite to resolve against.
     But `box-sizing: border-box` means this element's own max-height INCLUDES
     its 1px border, top and bottom, and the panel's did not: at a 900px
     viewport the cap is 810px, the dialog's content box is 808px, and the
     panel stood at exactly 810. Two pixels of overflow, and the UA stylesheet
     gives every <dialog> `overflow: auto`, so Chromium drew a full-height
     scrollbar down the edge of every dialog in the product for those two
     pixels. Measured on five component types: client 808, scroll 810, on all of
     them. It was never about the component form.

     Stated as a subtraction (`calc(var(--dialog-max-block) - 2px)`) it would
     have been correct and would have kept the duplication that caused it.
     With one `minmax(0, 1fr)` row the panel is stretched to whatever this
     element's content box turns out to be, border and all, so the cap is said
     once and cannot drift from itself again.

     On `[open]` only: `dialog:not([open])` is `display: none` in the UA
     stylesheet, and an author `display` here would override it and paint every
     closed dialog in the document. */
  .dialog[open] {
    display: grid;
    grid-template-rows: minmax(0, 1fr);
    opacity: 1;
    transform: none;
  }

  @starting-style {
    .dialog[open] {
      opacity: 0;
      transform: scale(0.97) translateY(var(--space-2));
    }
  }

  .dialog::backdrop {
    background: color-mix(in oklab, black 55%, transparent);
    opacity: 0;
    transition: opacity var(--dur-3) var(--ease-out);
  }
  .dialog[open]::backdrop { opacity: 1; }

  /* `min-block-size: 0`, and it is the half of the rule above that is easy to
     leave out: a grid item's automatic minimum size is its content, so without
     this the panel would refuse to shrink into the `minmax(0, 1fr)` row and
     the two pixels would come straight back. `.dialog__body` needs no such
     override - it is `overflow-y: auto`, which already floors its automatic
     minimum at zero. */
  .dialog__panel {
    display: grid;
    grid-template-rows: auto 1fr auto;
    min-block-size: 0;
    overscroll-behavior: contain;
  }

  .dialog__head {
    display: flex;
    align-items: start;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-5) var(--space-5) 0;
  }

  .dialog__title {
    font-size: var(--text-xl);
    line-height: var(--text-xl-lh);
    margin: 0;
    text-wrap: balance;
  }

  /* A caller's own node on the title line (openDialog's titleControl). It
     takes the room the heading would have, so the actions still sit at the
     end of the row. */
  .dialog__title-control {
    flex: 1 1 auto;
    min-inline-size: 0;
  }

  /* The actions, when a caller asks for them on the title line rather than in
     a footer (openDialog's actionsPlacement). `margin-inline-start: auto` on
     the group rather than on the title, so the buttons travel together however
     many of them there are. */
  .dialog__head-actions {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    gap: var(--space-3);
    margin-inline-start: auto;
    /* The header aligns to `start` so a title that wraps to two lines
       still begins level with what sits beside it. Buttons want the
       optical centre of whatever the title turned out to be. */
    align-self: center;
  }

  /* Below `sm` the actions take their own line. Two buttons and a title share
     a 390px row only while the title is short, and a component called "The Siege
     of Bramblehold Keep" wraps to four lines beside them. The wrap keeps the
     buttons reachable at a readable size instead. */
  @media (max-width: 39.99rem) {
    .dialog__head { flex-wrap: wrap; }
    .dialog__head-actions {
      flex: 1 0 100%;
      justify-content: flex-end;
      margin-inline-start: 0;
    }
  }

  .dialog__close {
    flex: 0 0 auto;
    width: calc(32px * var(--ui-scale));
    height: calc(32px * var(--ui-scale));
    display: grid;
    place-items: center;
    border-radius: var(--radius-pill);
    color: var(--text-muted);
    font-size: var(--text-lg);
    line-height: 1;
    cursor: pointer;
    transition: background-color var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out);
  }
  .dialog__close:hover { background: var(--surface-2); color: var(--text); }
  .dialog__close:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

  .dialog__body {
    padding: var(--space-5);
    overflow-y: auto;
    overscroll-behavior: contain;
    display: grid;
    gap: var(--space-4);
  }

  .dialog__actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-3);
    padding: 0 var(--space-5) var(--space-5);
  }

  /* AN ACTION WAITING ON THE SERVER. `openDialog` disables the row and calls
     ui/button's `setLoading` on the pressed control while an async `onSelect`
     is in flight, so a second press cannot run the action twice (bl-67).

     DESIGN.md section 4 states the pending button in one sentence, and it has
     two halves: "keeps its label and adds a spinner. It never becomes a bare
     spinner, AND ITS WIDTH DOES NOT CHANGE." `setLoading` prepends the spinner
     as a flex item, which answers the first half and breaks the second -
     measured 130px -> 157px on the publish dialog's own confirm button. The
     row jumps and the control moves out from under the pointer that just
     pressed it, at the exact moment a person is watching to see whether the
     press took.

     So the spinner comes OUT OF FLOW and sits in the leading padding the
     button already has. `.btn` is `position: relative` in its own base rule,
     which is what this hangs off. The inset centres it in that padding
     (`--space-5` wide, spinner `1em`), derived rather than measured, so it
     follows the two values it is between if either moves.

     `translate`, NOT `transform`, and that is load-bearing rather than
     modern-for-its-own-sake: `.btn__spinner`'s own `btn-spin` animation
     animates `transform`, so centring it with `transform: translateY(-50%)`
     would be overwritten the moment the animation started and the spinner
     would sit half a line low - or, under `prefers-reduced-motion`, where the
     animation is switched off, it would not. A bug that only appears when
     motion is ON is the worst kind to go looking for.

     ui/button's `setLoading` has the same in-flow widening wherever else it is
     used - five admin sections - and the general fix belongs in that
     component. This is the dialog's share of it. */
  .dialog__actions .btn[data-loading] .btn__spinner,
  .dialog__head-actions .btn[data-loading] .btn__spinner {
    position: absolute;
    inset-inline-start: calc((var(--space-5) - 1em) / 2);
    inset-block-start: 50%;
    translate: 0 -50%;
  }

  @media (prefers-reduced-motion: reduce) {
    .dialog {
      transition: opacity var(--dur-2) linear;
      transform: none;
    }
    @starting-style {
      .dialog[open] { opacity: 0; transform: none; }
    }
    .dialog::backdrop { transition: opacity var(--dur-2) linear; }
  }
}
