@layer components {
  /* Buttons.
   *
   * Every dimension is em, measured against the button's own font-size, so a
   * size variant sets font-size and the padding, gap, and icon all follow:
   *
   *   .btn--small { --btn-text: var(--text-small); }
   *
   * One declaration instead of five, and the small button can't drift out of
   * proportion with the normal one because it isn't described separately.
   *
   * Padding comes from --control-padding-*, which form fields use too, so a
   * button and an input sitting next to each other are the same height without
   * either one knowing the other exists.
   *
   * Colour is four custom properties. A new tone is a four-line block and
   * never touches markup — see VIEWS.md on variants as modifiers.
   */
  .btn {
    --btn-text: 1em;
    --btn-background: var(--color-canvas);
    --btn-border-color: var(--color-border);
    --btn-color: var(--color-ink);
    --btn-hover-brightness: 0.97;
    --btn-icon-size: 1.15em;

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--control-gap);
    padding: var(--control-padding-block) var(--control-padding-inline);
    border: var(--border-width) solid var(--btn-border-color);
    border-radius: var(--radius-pill);
    background: var(--btn-background);
    color: var(--btn-color);
    font: inherit;
    font-size: var(--btn-text);
    font-weight: var(--weight-medium);
    line-height: var(--leading-normal);
    white-space: nowrap;
    text-decoration: none;
    cursor: pointer;
    transition: background var(--transition-fast), border-color var(--transition-fast), filter var(--transition-fast);

    /* Only where there is a real pointer. On a touch screen :hover sticks
       after the tap and the button stays dimmed until you tap elsewhere. */
    @media (any-hover: hover) {
      &:hover {
        filter: brightness(var(--btn-hover-brightness));
      }
    }

    /* Inert, not faded. Dropping the opacity of a solid tone takes the fill and
       the label down together: .btn--negative lands on a pale pink wearing
       near-white text, which reads as a colour that went wrong rather than as a
       control you can't use. Every tone disables to the same neutral instead. */
    &:disabled,
    &[aria-disabled="true"] {
      --btn-background: var(--surface-sunk);
      --btn-border-color: transparent;
      --btn-color: var(--color-ink-soft);
      --btn-hover-brightness: 1;

      cursor: not-allowed;
    }

    svg {
      inline-size: var(--btn-icon-size);
      block-size: var(--btn-icon-size);
      flex-shrink: 0;
    }
  }

  /* Tones */

  .btn--primary {
    --btn-background: var(--color-accent);
    --btn-border-color: var(--color-accent);
    --btn-color: var(--color-accent-ink);
    --btn-hover-brightness: 1.15;
  }

  /* Destructive. Solid, because the one thing worse than an operator hesitating
     over a delete is an operator not noticing it. Pair with a confirm — see
     confirm_controller.js. */
  .btn--negative {
    --btn-background: var(--color-negative);
    --btn-border-color: var(--color-negative);
    --btn-color: var(--color-accent-ink);
    --btn-hover-brightness: 1.1;
  }

  /* The quiet one: no chrome until you point at it. For a destructive action
     that shouldn't shout, add the .txt-negative utility. */
  .btn--subtle {
    --btn-background: transparent;
    --btn-border-color: transparent;
    --btn-color: var(--color-ink-soft);
    --btn-hover-brightness: 1;

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

  /* Sitting on top of media. A photo can be any colour, so the button brings a
     translucent slab of canvas with it rather than trusting what is underneath;
     pair it with a text tone for the label. */
  .btn--overlay {
    --btn-background: var(--color-canvas-translucent);
    --btn-border-color: transparent;
  }

  /* Sizes — the whole variant, because everything inside is em. */

  .btn--small {
    --btn-text: var(--text-small);
  }

  .btn--large {
    --btn-text: var(--text-medium);
  }

  /* Shapes */

  /* Square by construction: --control-size is the natural height of a control
     expressed as a formula, so the width matches the height and the button
     stays in line with text buttons without either one naming a size. */
  .btn--icon {
    inline-size: var(--control-size);
    padding-inline: 0;
  }

  .btn--block {
    inline-size: 100%;
  }

  /* Submitting.
   *
   * Turbo marks the form aria-busy for the length of the request, so a button
   * shows the wait without any JavaScript of ours. This matters more here than
   * in most apps: a submit that reaches a channel can take a couple of seconds,
   * and an operator who thinks nothing happened will click again.
   */
  form[aria-busy="true"] .btn[type="submit"] {
    cursor: progress;
    pointer-events: none;
  }

  /* The spinner needs a pseudo-element, which void elements don't render, so it
     lands only on button.submit — not on the input.submit that form.submit
     produces. Both get the busy cursor above; this is the extra. */
  form[aria-busy="true"] button.btn {
    --btn-spinner-size: 1em;
    /* Its own, rather than --duration-slow: a spinner is not travelling
       anywhere, and slow motion should not leave it crawling. */
    --btn-spinner-duration: 700ms;

    position: relative;
    color: transparent;

    &::after {
      content: "";
      position: absolute;
      inset-block-start: 50%;
      inset-inline-start: 50%;
      margin-block-start: calc(var(--btn-spinner-size) / -2);
      margin-inline-start: calc(var(--btn-spinner-size) / -2);
      inline-size: var(--btn-spinner-size);
      block-size: var(--btn-spinner-size);
      border: var(--border-width-thick) solid var(--btn-color);
      border-block-start-color: transparent;
      border-radius: var(--radius-circle);
      animation: btn-spin var(--btn-spinner-duration) linear infinite;
    }
  }

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

  @media (prefers-reduced-motion: reduce) {
    form[aria-busy="true"] button.btn::after {
      animation-duration: 0s;
    }
  }
}
