/* Buttons and action rows. One filled button per screen, at the epicenter,
   filled ink and never shu (HANSHI.md, Buttons).

   ONE BUTTON, RETUNED BY CUSTOM PROPERTIES. Every variant below sets three or
   four --btn-* values and nothing else; no variant restates a declaration. Both
   Basecamp and Fizzy arrived at this independently (`.btn--plain{--btn-background:
   transparent}`, `.btn--negative{--btn-background:var(--color-negative)}`), and
   the reason shows up the third time you add a variant: with plain declarations
   each one has to remember padding, radius, border and both hover states, and
   they drift. Here the base owns the anatomy and a variant owns the palette.

   There is no separate icon-button system. HANSHI says "icons first: 26–28px
   circles", which is a button whose padding is 0 and whose radius is 50% — so
   it is .btn--circle, and it is also auto-detected, Fizzy-style, for any button
   that carries an icon and names itself with aria-label. */
@layer components {
  .btn {
    /* The four a variant retunes. */
    --btn-background: var(--color-ink);
    --btn-color: var(--color-canvas);
    --btn-border-color: transparent;
    --btn-background-hover: var(--color-ink-subtle);

    /* Anatomy — shared by every variant, which is the point. The mass is
       deliberately below the handoff's: full-size type over half-block padding
       measured 42px tall and read "massive" (user, 2026-08-11) — so a button
       speaks one size down and ~31px tall, subtle beside the text it acts on.
       Recorded under Deviations in HANSHI.md. The block padding is a fixed
       rem, not a half token: a control's size is its own, never the hosting
       surface's density. */
    --btn-color-hover: var(--btn-color);
    --btn-padding: 0.25rem var(--inline-space-double);
    --btn-radius: var(--border-radius);
    --btn-size: 1.75rem; /* the circle's diameter, 28px */

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--inline-space-half);
    padding: var(--btn-padding);
    font-size: var(--text-small);
    font-weight: 500;
    /* An <input type="submit"> renders its value itself rather than as a child
       box, so the flex alignment above cannot reach it. */
    text-align: center;
    /* A control's label never wraps; a squeezed row scrolls or reflows around
       the button, not through it. */
    white-space: nowrap;
    color: var(--btn-color);
    background-color: var(--btn-background);
    border: var(--border-size) solid var(--btn-border-color);
    border-radius: var(--btn-radius);
    text-decoration: none;
    cursor: pointer;

    &:hover {
      color: var(--btn-color-hover);
      background-color: var(--btn-background-hover);
    }

    /* A disabled button must not light up under the pointer — :hover still
       fires on one. The move-up and move-down controls on a card are disabled
       at the ends of their column (records/_entry_stages.html.erb), so this is
       a live state rather than a hypothetical. */
    &:disabled {
      --btn-background-hover: var(--btn-background);
      --btn-color-hover: var(--btn-color);

      opacity: 0.4;
      cursor: default;
    }

    /* ——— Working ———
       Turbo already says when a form is in flight, so a spinner needs no
       controller, no data attribute and no markup: on submit it disables the
       SUBMITTER and sets aria-busy on the FORM, and undoes both when the
       response lands. Verified in the bundle we serve, not assumed —
       `requestStarted` runs `forms.submitter.beforeSubmit` (the shipped
       default strategy is "disabled") and then marks the form busy. Ported
       from ../chalet, which discovered the pair.

       Scoped to `form[aria-busy]` rather than any busy ancestor, which is the
       one place ours departs from theirs: Turbo also marks
       document.documentElement busy for the length of every Drive visit, so
       the unscoped rule spins every genuinely disabled button on the page
       whenever you navigate — and we have real ones (the move controls at the
       ends of a column). A form is the only busy thing that means "this
       button is working".

       Cost of the whole feature: this rule and the drawing in spinner.css.

       The label goes transparent rather than being hidden as a child, because
       an <input type="submit"> draws its value itself and has no child to
       hide — the same reason the flex alignment above cannot reach it. Full
       opacity, because 0.4 is how a button says "unavailable" and this one is
       saying the opposite. Its width never changes, so no row jumps. */
    form[aria-busy] &:disabled {
      /* The dots themselves are spinner.css, which lists this selector.
         --dot-color is the button's own ink and not currentColor, because
         currentColor is transparent here, by the line below. */
      --dot-color: var(--btn-color);
      --dot-inset: 50%;
      --dot-speed: 1s;

      position: relative;
      opacity: 1;
      color: transparent;
    }
  }

  /* Secondary actions: a quiet bordered label with no fill. */
  .btn--quiet {
    --btn-background: transparent;
    --btn-color: var(--color-ink);
    --btn-border-color: var(--color-line);
    --btn-background-hover: var(--color-surface);
  }

  /* A verb that reads as a line of prose rather than a control — "remove my
     picture and show my initials instead", sitting under the picture it talks
     about. Fizzy's .btn--plain, and the reason it exists here: a bordered box
     around a sentence that long is a button the size of the section it belongs
     to (user, 2026-08-13). It is still a button, so it is still .btn; all it
     gives up is the box.

     The card board's column expander wears it too, and needs the ink to be
     inherited rather than quieted — it sits on the column's own gradient. That
     retune lives with the column, in card-columns.css. */
  .btn--plain {
    --btn-background: transparent;
    --btn-color: var(--color-ink-subtle);
    --btn-color-hover: var(--color-ink);
    --btn-background-hover: transparent;
    --btn-padding: 0;
  }

  /* The one filled button that is filled in the LINK colour rather than ink.
     It exists for the card board, where "Add a card" is the epicentre of a
     surface that is painted in fizzy's blue rather than our ink — see
     card-board-tokens.css for why that palette is scoped there. Everywhere
     else, HANSHI's one-filled-button-per-screen rule still means `.btn`. */
  .btn--link {
    --btn-background: var(--color-link);
    --btn-color: var(--color-canvas);
    --btn-border-color: var(--color-canvas);
    --btn-background-hover: var(--color-link);
  }

  /* Icons first: a circle at the top of the spec's 26–28px range, ink-subtle at
     rest, hover = the surface wash plus full ink. Worn by <a> and <label> as
     well as <button>, so it has to kill the underline a link carries.

     The second selector is Fizzy's auto-detection: a button that is nothing but
     an icon has to name itself with aria-label anyway, so the two together are
     already an unambiguous description of a circle — no third class to forget.
     The assignees strip's + trigger (recordings/_assignees) rides it. */
  .btn--circle,
  .btn:where([aria-label]:not(.btn--circle-mobile)):has(.icon) {
    --btn-background: transparent;
    --btn-color: var(--color-ink-subtle);
    --btn-color-hover: var(--color-ink);
    --btn-border-color: transparent;
    --btn-background-hover: var(--color-surface);
    --btn-padding: 0;
    --btn-radius: 50%;

    inline-size: var(--btn-size);
    block-size: var(--btn-size);
    aspect-ratio: 1;
    flex-shrink: 0;
    font-size: var(--text-medium);
    font-weight: 400;
  }

  /* On a touch screen the disc keeps its drawn size — HANSHI rules 26–28px and
     a review is no place to redraw it — and grows an invisible halo out to
     --tap-target-size instead. Fizzy's .btn--ensure-tap-target-size, except it
     is not a class anyone has to remember: every circle in the app is an
     icon-only action, so every circle wants the finger room.

     Adjacent circles sit 38px apart at gap-half, so two halos overlap by ~6px
     and the later one in the DOM takes that sliver. Still far better than the
     28px each of them offered before.

     .todo-check joins the list from todos.css, the way labels.css lists
     foreign selectors: HANSHI rules the tick and the circles in one paragraph
     and the answer is the same — 20px drawn (16px on a subtask), which is
     under even the WCAG floor, so the ink stays and the finger gets the
     halo. */
  @media (hover: none) {
    .btn--circle,
    .btn:where([aria-label]):has(.icon),
    .todo-check {
      /* .btn--circle-mobile is deliberately absent: wide, it is its words
         and already finger-sized; narrow, its own variant block below
         brings the halo along with the circle. */
      position: relative;

      &::before {
        content: "";
        position: absolute;
        inset-block-start: 50%;
        inset-inline-start: 50%;
        translate: -50% -50%;
        inline-size: var(--tap-target-size);
        block-size: var(--tap-target-size);
      }
    }
  }

  /* ——— fizzy's .btn--circle-mobile, on the shell's own break ———
     A labelled header action that folds to its icon on a narrow sheet, so
     the action row still fits beside the heading (the set page's New list).
     Wide, it is its words and the icon stays out of the picture; narrow,
     the label hides — fizzy's own selector — and the button takes the
     circle treatment above, restated here because a @container query cannot
     join that rule's selector list. It is excluded from the icon-circle
     auto-detection (the :not up there): carrying an aria-label and an .icon
     at every width, it would read as a circle on a desk too. A @container
     query and not fizzy's @media: #main narrows just the same when the
     panel opens (AGENTS.md). */
  @container main (width >= 35rem) {
    .btn--circle-mobile > .icon {
      display: none;
    }
  }

  @container main (width < 35rem) {
    .btn--circle-mobile {
      --btn-background: transparent;
      --btn-color: var(--color-ink-subtle);
      --btn-color-hover: var(--color-ink);
      --btn-border-color: transparent;
      --btn-background-hover: var(--color-surface);
      --btn-padding: 0;
      --btn-radius: 50%;

      inline-size: var(--btn-size);
      block-size: var(--btn-size);
      aspect-ratio: 1;
      flex-shrink: 0;
      font-size: var(--text-medium);

      > span:not(.icon) {
        display: none;
      }

      /* The same halo every circle grows under a finger (the block above). */
      @media (hover: none) {
        position: relative;

        &::before {
          content: "";
          position: absolute;
          inset-block-start: 50%;
          inset-inline-start: 50%;
          translate: -50% -50%;
          inline-size: var(--tap-target-size);
          block-size: var(--tap-target-size);
        }
      }
    }
  }

  /* Action rows right-align, primary outermost. `.modal__actions` used to be
     listed here so a dialog's row would stop restating all five declarations;
     it is gone now, because a dialog's row simply IS this row — the forms say
     `.actions` and the only thing that was ever a modal's own, the space above
     it, comes from the dialog's own stack (2026-08-29). */
  .actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--inline-space);
  }

  /* Except a SUB-form's row, which sits left with its own fields — BC's
     shape. Right-aligned it landed on the sheet form's Save row directly
     beneath and read as a duplicate (user, 2026-08-11). Primary outermost
     still holds: on a left row the outer edge is the left. */
  .actions--nested {
    justify-content: flex-start;
    /* A top-level row is spaced by the .stack it sits in; a nested one sits
       inside a <dd> with nothing between it and the field above, so it clears
       that field itself. It showed the moment the sub-form's date grew a box
       and the buttons sat on its edge (user, 2026-08-29). */
    margin-block-start: var(--block-space-half);
  }

  /* A row with TWO submits lists the primary first so return picks it —
     implicit submission takes the first submit button in the form — then
     reverses so the primary still stands outermost. Every add-form with a
     "save and add another" wears it, dialogs included; the sheet needed it the
     day the composer grew Save a draft. */
  .actions--reversed {
    flex-direction: row-reverse;
    justify-content: flex-start;
  }

  /* A keyboard hint sits far left. base already gives <kbd> the mono family, so
     it needs only the x-small size, the faint ink, and the auto margin that
     eats the free space beside it. */
  .actions kbd {
    margin-inline-end: auto;
    font-size: var(--text-x-small);
    color: var(--color-ink-faint);
  }
}
