@layer components {
  /* Dialogs — one component, three shapes.
   *
   * A centred modal, a drawer off the inline edge, and a sheet off the bottom
   * differ in where they rest and how far they travel to get there. That is
   * position and translate; the markup, the controller, and every other
   * property are identical. Which is the point: there is no separate modal,
   * drawer, or sheet component to keep in step, and a fix to the dismissal
   * behaviour of one is a fix to all three.
   *
   * Built on <dialog>, so the platform supplies the parts that are easy to get
   * subtly wrong: the top layer (no z-index to fight), Escape, the focus trap,
   * inert background content, and returning focus to whatever opened it.
   * dialog_controller.js adds only what the element leaves out.
   */
  .dialog {
    --dialog-width: 32rem;
    --dialog-radius: var(--radius-large);
    --dialog-duration: var(--duration);
    --dialog-easing: var(--ease-out);
    --dialog-max-block-size: 85dvh;
    --dialog-scale: 0.96;
    --dialog-translate: none;
    --dialog-backdrop-opacity: 35%;
    --dialog-backdrop-blur: 2px;

    /* The column lives here, not on [open], and that distinction is load
       bearing. Only `display` can be conditional: it is what keeps a closed
       dialog off the page, and transition-behavior holds it through the close.
       flex-direction gets no such protection — declared on [open] it snapped
       back to `row` the instant the attribute went, so for the whole closing
       animation the header, body and footer sat side by side as full-height
       columns and the footer stretched every button to the height of the
       dialog. Anything here that isn't `display` belongs on the base rule. */
    flex-direction: column;
    padding: 0;
    border: 0;
    border-radius: var(--dialog-radius);
    background: var(--color-canvas);
    color: var(--color-ink);
    box-shadow: var(--shadow-overlay);
    inline-size: var(--dialog-width);
    max-inline-size: calc(100dvw - var(--space-double));
    max-block-size: var(--dialog-max-block-size);
    overflow: hidden;

    opacity: 0;
    scale: var(--dialog-scale);
    translate: var(--dialog-translate);
    transition-property: display, overlay, opacity, scale, translate;
    transition-duration: var(--dialog-duration);
    transition-timing-function: var(--dialog-easing);
    transition-behavior: allow-discrete;

    /* Laid out as a column only while open. A closed dialog has to keep the
       display: none it gets from the browser, or it sits on the page. */
    /* `scale: none` rather than `scale: 1`, and the difference is not cosmetic:
       any scale that is not `none` makes this element the containing block for
       every `position: fixed` descendant. A combobox inside a dialog is fixed
       precisely so that nothing clips it (see popover.css), and a dialog resting
       at `scale: 1` captured it — its list opened against the dialog's own box
       instead of the window. `none` interpolates as 1, so the entrance still
       animates from --dialog-scale. */
    &[open] {
      display: flex;
      opacity: 1;
      scale: none;
      translate: none;
    }

    /* Contents that arrive from the server arrive in a frame, and a frame
       between the dialog and its parts would be the flex child instead of
       them. Transparent to the layout, and still there for the selectors
       below — which is why they have to see through it as well. */
    > turbo-frame {
      display: contents;
    }

    /* The body and the actions usually sit inside one form — the submit button
       has to be in it — so the form carries the column too. Without this the
       body isn't a flex child of the dialog and stops being the part that
       scrolls. */
    > form,
    > turbo-frame > form {
      display: flex;
      flex: 1 1 auto;
      flex-direction: column;
      min-block-size: 0;
    }

    &::backdrop {
      background: var(--color-ink);
      opacity: 0;
      backdrop-filter: blur(var(--dialog-backdrop-blur));
      transition-property: display, overlay, opacity;
      transition-duration: var(--dialog-duration);
      transition-timing-function: var(--dialog-easing);
      transition-behavior: allow-discrete;
    }

    &[open]::backdrop {
      opacity: var(--dialog-backdrop-opacity);
    }

    /* Where the opening animation starts. Without this the dialog already
       holds its final values by the time it enters the top layer, and there is
       nothing for the transition to move from. */
    @starting-style {
      &[open] {
        opacity: 0;
        scale: var(--dialog-scale);
        translate: var(--dialog-translate);
      }

      &[open]::backdrop {
        opacity: 0;
      }
    }
  }

  /* A modal dialog is the only thing on the page you can touch, so the page
     behind it shouldn't scroll away underneath. */
  html:has(dialog:modal) {
    overflow: hidden;
  }

  .dialog__header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space);
    padding: var(--space-and-half) var(--space-and-half) var(--space);
  }

  .dialog__title {
    margin: 0;
    font-size: var(--text-medium);
    font-weight: var(--weight-bold);
  }

  /* A strip under the header for a control that changes what the dialog is
     about rather than editing it. Outside the body because it must not scroll
     away with it, and outside the form below because a form cannot contain
     another one — which is exactly the case it exists for: a GET that
     re-addresses the dialog, above a PATCH that writes it. */
  .dialog__toolbar {
    display: grid;
    gap: var(--space-half);
    flex: 0 0 auto;
    padding-inline: var(--space-and-half);
    padding-block-end: var(--space);
  }

  /* The only part that scrolls. A long body must not push the actions off the
     bottom of the screen, where an operator can't reach them.
   *
     It also spaces what is in it, which .form cannot do from out here: the rule
     above makes a form inside a dialog a flex column, so a form's own grid gap
     never applies to the fields — they were separated by nothing at all, and a
     drawer of labels and boxes ran together into one block. align-content
     because this box takes the height the drawer has left: auto rows in a grid
     with room to spare stretch, which spread four fields evenly down a phone. */
  .dialog__body {
    display: grid;
    align-content: start;
    /* And every row sized to what is in it, which `auto` does not guarantee: a
       grid item with `overflow: hidden` has an automatic minimum size of zero, so
       an auto track holding one can be sized under its content. Put three
       .panel--flush sections in here and the body squeezed all three to one
       height, clipped the last row out of each, and reported nothing to scroll —
       so the part that is supposed to be the only scrolling part of a dialog
       silently was not. */
    grid-auto-rows: min-content;
    gap: var(--space);
    flex: 1 1 auto;
    min-block-size: 0;
    padding-inline: var(--space-and-half);
    padding-block-end: var(--space-and-half);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

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

  /* Shapes */

  /* Off an inline edge, full height. Which edge is the only difference between
     the two, so the shape is declared once and each says where it rests and
     which way it travels to get there. Navigation takes the start one: it lives
     on that side of the screen, and a nav that slid in from the opposite edge
     would land somewhere it had never been. */
  .dialog--drawer,
  .dialog--drawer-start {
    --dialog-width: 28rem;
    --dialog-radius: 0;
    --dialog-scale: 1;
    --dialog-max-block-size: 100dvh;
    --dialog-duration: var(--duration-slow);
    --dialog-easing: var(--ease-emphasized);

    position: fixed;
    inset-block: 0;
    margin: 0;
    block-size: 100dvh;
  }

  .dialog--drawer {
    --dialog-translate: 100% 0;

    inset-inline: auto 0;
  }

  .dialog--drawer-start {
    --dialog-translate: -100% 0;

    inset-inline: 0 auto;
  }

  /* Up from the bottom. */
  .dialog--sheet {
    --dialog-width: 100%;
    --dialog-radius: var(--radius-large) var(--radius-large) 0 0;
    --dialog-scale: 1;
    --dialog-translate: 0 100%;
    --dialog-max-block-size: 90dvh;
    --dialog-duration: var(--duration-slow);
    --dialog-easing: var(--ease-emphasized);

    position: fixed;
    inset-block-start: auto;
    inset-block-end: 0;
    inset-inline: 0;
    margin: 0;
    max-inline-size: none;
  }

  /* Sizes */

  .dialog--narrow {
    --dialog-width: 24rem;
  }

  .dialog--wide {
    --dialog-width: 44rem;
  }

  /* Below the small band a side drawer would be the full width of the screen
     anyway, and arriving from the bottom puts the top of it under the thumb
     rather than at the far end of a reach. */
  @media (width < 40rem) {
    .dialog--drawer,
    .dialog--drawer-start {
      --dialog-width: 100%;
      --dialog-radius: var(--radius-large) var(--radius-large) 0 0;
      --dialog-translate: 0 100%;
      --dialog-max-block-size: 90dvh;

      inset-block-start: auto;
      inset-inline: 0;
      block-size: auto;
      max-inline-size: none;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .dialog,
    .dialog::backdrop {
      transition-duration: 0s;
    }
  }
}
