@layer components {
  /*
  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
   🅸🅽🅿🆄🆃 🆂🆃🅴🅿🅿🅴🆁
  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
  - https://www.nngroup.com/articles/input-steppers/

  @scope keeps these rules from leaking OUT, and the reset below strips UA default
  chrome off the generated buttons / input / track divs so the component styles from
  a clean slate. Caveat: this lives in @layer components, so it only shields the
  internals from UA defaults and lower-layer rules — *unlayered* host-page rules (a
  CSS reset, Tailwind preflight, naked `button {}`) still outrank every cascade layer
  and are NOT neutralized here.
  */

  @scope (l-input-stepper) {
    /* Reset — :where() keeps specificity at (0,0,0) so the component rules below win
       over it. Strips UA default styling off the generated internals for a clean
       slate. Custom properties are NOT affected by `all` — design tokens cascade
       through. :scope is excluded — the host is a unique custom element name and gets
       its styles from the base + appearance :scope blocks without interference. */
    *:where(:not(img, svg, l-icon):not(svg *)),
    *::before,
    *::after {
      all: unset;
      display: revert;
    }

    /*
    ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
     🅷🅾🆂🆃
    ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
    */

    :scope {
      --_button-size: var(--l-size-control-md);
      --border-color: var(--l-form-control-border-color);
      --border-radius: var(--l-form-control-border-radius);
      --_button-bg-hover: var(--l-color-bg-fill-neutral-soft);
      --_button-bg-active: var(--l-color-bg-fill-neutral-subtle);

      display: inline-flex;
      align-items: stretch;
      white-space: nowrap;
      box-sizing: border-box;
    }

    /*
    ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
     🅵🅾🆄🅲🅴 🅿🆁🅴🆅🅴🅽🆃🅸🅾🅽
    ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
    Reserve button space before JS loads to avoid layout shift when buttons mount.
    */

    :scope:not(:defined) {
      padding-inline: var(--_button-size);
    }

    :scope:defined {
      padding-inline: 0;
    }

    /*
    ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
     🆂🅸🆉🅴🆂
    ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
    */

    :scope[size='xs'] {
      --_button-size: var(--l-size-control-xs);
    }
    :scope[size='sm'] {
      --_button-size: var(--l-size-control-sm);
    }
    :scope[size='lg'] {
      --_button-size: var(--l-size-control-lg);
    }
    :scope[size='xl'] {
      --_button-size: var(--l-size-control-xl);
    }

    /*
    ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
     🅱🆄🆃🆃🅾🅽🆂
    ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
    */

    /*
    Appearance files re-style these internals, and each of them `@import`s this file,
    so a page loading two appearances ends up with two copies of the block below —
    the second one sitting *after* the first appearance's own rules. Element selectors
    like `button` carry no extra specificity from the `@scope` prelude, so that later
    copy would win on source order and strip the first appearance's skin.

    Contract: an appearance file qualifies every rule that targets a shared internal
    with `:scope ` (`:scope button`, not `button`). That single pseudo-class puts it
    permanently above this block, whatever order the imports land in.
    */

    button {
      padding: 0;
      appearance: none;
      background: none;
      border: 0;
      font-family: inherit;
      font-size: inherit;
      width: var(--_button-size);
      aspect-ratio: 1;
      cursor: pointer;
      touch-action: manipulation;
      -webkit-tap-highlight-color: transparent;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    button:focus-visible {
      outline: 2px solid var(--l-focus-ring);
      outline-offset: -2px;
      z-index: 1;
    }

    /* The buttons hold nothing but a currentColor icon, so the disabled ink
       token does the whole job — and undiluted, unlike the opacity fade it
       replaces, which also dimmed the border token the `rounded` appearance
       sets on this same state. Matches `input:disabled` below. */
    button:disabled {
      cursor: not-allowed;
      color: var(--l-form-control-disabled-color);
    }

    @media (hover: hover) {
      button:hover:not(:disabled) {
        background-color: var(--_button-bg-hover);
      }
    }

    button:active:not(:disabled) {
      background-color: var(--_button-bg-active);
    }

    /*
    ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
     🅸🅽🅿🆄🆃
    ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
    */

    input {
      appearance: textfield;
      box-sizing: border-box;
      font-family: inherit;
      font-size: inherit;
      font-variant-numeric: tabular-nums;
      border: 0;
      text-align: center;
      width: calc(3ch + var(--_button-size) * 2 / 3);
      min-width: 0;
      padding-inline: calc(var(--_button-size) / 3);
      align-self: stretch;
      background: transparent;
      color: inherit;
      caret-shape: underscore;
    }

    input:disabled {
      cursor: not-allowed;
      color: var(--l-color-text-disabled);
    }

    /*
    ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
     🆃🆁🅰🅲🅺 (animated number display)
    ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
    */

    .l-input-stepper-value {
      position: relative;
      display: flex;
      align-items: center;
      min-width: 0;
    }

    /* No background: the overlay REPLACES the input's digits (see the opacity rule
       below) instead of masking them behind an opaque fill, so whatever the host
       paints — the form-control fill in `default`, nothing at all in `rounded` —
       shows through untouched. A fill here would composite a second time over the
       host's own and read as a tinted rectangle over the value area wherever either
       one is translucent, e.g. --l-form-control-disabled-background. */
    .l-input-stepper-track-display {
      position: absolute;
      inset: 0;
      height: var(--_button-size);
      overflow: hidden;
      pointer-events: none;
      font-variant-numeric: tabular-nums;
    }

    .l-input-stepper-track {
      transition: translate 0.26s ease-out;
    }

    .l-input-stepper-track div {
      display: flex;
      align-items: center;
      justify-content: center;
      height: var(--_button-size);
    }

    /* Hide the real digits while the roller is the thing on screen. `opacity`, NOT
       `visibility`/`display` — those drop the input out of the tab order, so the
       :focus-within rule below could never fire to swap the two back. Gated on both
       the attribute and the overlay's presence because the overlay is built once at
       setup: toggling `with-roller` afterwards must not blank the value. */
    :scope[with-roller]
      .l-input-stepper-value:has(.l-input-stepper-track-display):not(:focus-within)
      input {
      opacity: 0;
    }

    .l-input-stepper-value:focus-within .l-input-stepper-track-display {
      visibility: hidden;
    }

    :scope:not([with-roller]) .l-input-stepper-track-display {
      display: none;
    }

    /*
    ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
     🅳🅸🆂🅰🅱🅻🅴🅳
    ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
    */

    :scope:has(input:disabled) {
      color: var(--l-color-text-disabled);
    }

    /* Invalid — mirror the shared form control invalid border (set by
       `l-form-field` via `aria-invalid`, or authored directly). */
    :scope:has(input[aria-invalid='true']) {
      --border-color: var(--l-form-control-border-color-invalid);
    }
  }

  /* Hide native spinners — kept outside @scope because Safari ignores
     ::-webkit-*-spin-button rules nested inside @scope blocks. */
  l-input-stepper input::-webkit-inner-spin-button,
  l-input-stepper input::-webkit-outer-spin-button {
    appearance: none;
    display: none;
    margin: 0;
  }
}

@layer components {
  @scope (l-input-stepper[appearance='rounded']) {
    :scope {
      --_gap: 0.25rem;

      gap: var(--_gap);
    }

    /* FOUCE: the base reserves two bare button widths, which is right for an
       appearance whose buttons sit flush against the value. These do not — each
       carries a 1px border and a gap — so the reserved box has to grow by both
       or the stepper jumps 12px wider and 12px taller the moment it hydrates.
       Qualified with the attribute so it outranks the base rule (equal
       specificity otherwise) whichever order the appearance sheets load in. */
    :scope:not(:defined)[appearance='rounded'] {
      padding-inline: calc(var(--_button-size) + 2px + var(--_gap));
      min-height: calc(var(--_button-size) + 2px);
    }

    /* Every rule that re-styles a shared internal is qualified with `:scope ` — see
       the note above the button block in `_base.css`. Without it, the base copy
       inlined by a *later*-imported appearance would win on source order. */
    :scope button {
      border: 1px solid var(--border-color);
      border-radius: var(--l-radius-full);
      background-color: var(--l-form-control-background-color);
    }

    @media (hover: hover) {
      :scope button:hover:not(:disabled) {
        border-color: var(--l-form-control-border-color-hover);
        background-color: transparent;
      }
    }

    /* Hover and press share one border token, the system's own for a hovered
       form control. It reads at gray-500 in both modes; --l-color-text-primary,
       which these two states used to reach for, lands near-black on a white page
       and near-white on a dark one — too hard an edge for a hairline circle. The
       press is told apart by its fill and by the same shrink `pill` uses: both
       appearances hang their buttons as free-standing discs, so both can afford
       to press one into the surface. `default` cannot — its buttons sit flush
       inside the container's border, and shrinking one opens a gap in the corner
       of the box — which is why the shrink lives in these two files and not in
       `_base.css`. */
    :scope button:active:not(:disabled) {
      border-color: var(--l-form-control-border-color-hover);
      background-color: var(--_button-bg-active);
      scale: 0.94;
    }

    @media (prefers-reduced-motion: reduce) {
      :scope button:active:not(:disabled) {
        scale: none;
      }
    }

    :scope button:disabled {
      border-color: var(--l-form-control-disabled-border);
    }
  }
}
