@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 {
  /*
  A capsule track carrying the fill, with the two circular buttons inset into it
  and lifted off by a shadow — the iOS-style quantity picker. Where `rounded`
  drops the container entirely and outlines each button, `pill` does the
  opposite: the container is the only painted edge and the buttons are solid.
  */
  @scope (l-input-stepper[appearance='pill']) {
    :scope {
      /* Space between the track edge and the buttons. Applied as padding on the
         host, so it grows the capsule *around* the content row and leaves the row
         itself exactly --_button-size tall — which the roller depends on, since
         its rows and its JS-computed translate are both sized off that variable. */
      --_inset: 0.25rem;

      /* Hairline around each button, drawn as a ring rather than a border: the
         buttons compute their size from --_button-size with the content-box
         sizing `all: unset` leaves them, so a real border would add 2px to every
         one and desync the capsule from the space the FOUCE rules reserve.
         `divider` resolves to the track's own colour in dark mode, where it
         disappears — correct, since a gray-900 disc on a gray-700 track needs no
         help; the ring earns its keep in light mode, where a white disc sits on
         a near-white track and the shadow alone cannot hold its top edge. */
      --_ring: 0 0 0 1px var(--l-color-divider);

      /* Capsule fill, and asymmetric on purpose: the relationship between the
         track and the buttons inverts between modes. In dark the buttons are
         *darker* than the track, so the track has to stand well clear of the
         page for them to read as sitting in it — neutral-subtle (gray-700 on a
         gray-900 page) already does that. In light the buttons are *lighter*
         than the track, so the same token lands at gray-100, barely 0.03 from
         white, and the capsule dissolves against any near-white surface. Light
         therefore steps off the surface itself, which also keeps the capsule
         correct on a themed page rather than pinned to one grey. */
      --_fill: light-dark(
        color-mix(in oklab, var(--l-color-surface) 92%, var(--l-color-text-primary)),
        var(--l-color-bg-fill-neutral-subtle)
      );

      min-height: calc(var(--_button-size) + var(--_inset) * 2);
      border-radius: var(--l-radius-full);
      background-color: var(--_fill);

      /* Capsule edge, inset so it paints inside the border box. A real border
         would add 2px to a host that is box-sizing: border-box with a min-height
         computed from --_button-size and the inset, breaking the pre-hydration
         parity the :not(:defined) rule maintains. Weighted above the buttons'
         hairline on purpose — a container boundary should not read lighter than
         the lines inside it — and it lands on the same token as the box
         `default` draws, so the two appearances sit at the same edge weight.
         Reading --border-color also means the shared invalid rule in `_base.css`
         recolours this capsule for free, exactly as it does default's box. */
      box-shadow: inset 0 0 0 1px var(--border-color);
    }

    /* Both rules restate a `padding-inline` the base sets (it reserves button
       space before JS loads, then clears it), and `:scope:defined` alone ties
       with it — leaving these to win on source order only holds while `pill` is
       the last appearance a page imports. Repeating the attribute breaks the tie
       outright, the same guard `rounded.css` uses on its own FOUCE rule. */
    :scope:defined[appearance='pill'] {
      padding: var(--_inset);
    }

    /* FOUCE: the reserved space has to clear the inset too, so the capsule keeps
       its hydrated width and the buttons mount without shifting anything. */
    :scope:not(:defined)[appearance='pill'] {
      padding: var(--_inset) calc(var(--_button-size) + var(--_inset));
    }

    /* 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-radius: var(--l-radius-full);
      background-color: var(--l-form-control-background-color);
      box-shadow: var(--_ring), var(--l-shadow-sm);
      transition: box-shadow 0.15s ease-out;

      /* Both the ring and the shadow are drawn *outside* the button's box, so
         they spill 1px into the value area next door. With `with-roller` that
         area holds an absolutely-positioned, opaque overlay, which paints above
         non-positioned content and sliced the spill clean off — the disc looked
         cut on the side facing the number. Lifting the buttons puts them back on
         top; `_base.css` already does exactly this for the focus outline, which
         is outset for the same reason. Works without `position` because these are
         flex items, which honour z-index on their own. */
      z-index: 1;
    }

    /* Same gesture as `rounded`: on hover the button's outline steps up. That
       appearance has nothing but an outline to work with, so it was already the
       house signal for "this disc is live" — matching it here keeps the two from
       teaching different vocabularies for the same control, and both now land on
       the token the system names for exactly this state on a form control. It
       sits at gray-500 in both modes: unmistakable against the divider hairline
       it replaces, without the near-black (gray-700) / near-white (gray-200)
       edge that reaching all the way to the ink draws on a 36px disc. Elevation is deliberately left alone — the
       ring carries the state, and shadow-sm to shadow-md was never the part
       anyone noticed. */
    @media (hover: hover) {
      :scope button:hover:not(:disabled) {
        /* Pinned so the base's --_button-bg-hover cannot slip a tint in behind
           the ring — `rounded` pins its own fill on hover for the same reason.
           The ring is the whole signal; nothing else moves. */
        background-color: var(--l-form-control-background-color);
        box-shadow:
          0 0 0 1px var(--l-form-control-border-color-hover),
          var(--l-shadow-sm);
      }
    }

    /* Press holds hover's outline rather than hardening it again — `rounded` does
       the same — and adds what only this appearance can say: the disc drops the
       elevation it rests on and shrinks, pressing down into the capsule. That
       shrink is load-bearing now that hover and press share one ring colour: it
       is the only thing separating the two states, and on touch, where no hover
       exists at all, the only feedback a tap gets. `.l-button[data-press-effect]`
       presses instantly for the same reason. */
    :scope button:active:not(:disabled) {
      /* Cancels the hover easing for the duration of the press. Without it the
         elevation would fade out over 150ms while the scale snaps, so the two
         halves of the same gesture arrive at different times. Releasing still
         eases back, since the rule above carries the transition again. */
      transition: none;
      box-shadow: 0 0 0 1px var(--l-form-control-border-color-hover);
      scale: 0.94;
    }

    @media (prefers-reduced-motion: reduce) {
      :scope button {
        transition: none;
      }

      :scope button:active:not(:disabled) {
        scale: none;
      }
    }

    /* At a bound, the button sinks into the track instead of sitting on it as a
       dead white disc. The base already dims the icon to the disabled ink. */
    :scope button:disabled {
      background-color: transparent;
      box-shadow: none;
    }

    /* Deliberately *not* the --l-form-control-disabled-* tokens the `default`
       appearance greys out with: those are translucent, and this capsule needs
       one flat colour rather than a tint laid over whatever it happens to sit
       on. Soft lands between the page and the enabled fill in both modes —
       gray-50 under a white page, gray-800 over a gray-900 one — so the capsule
       recedes toward the surface instead of standing off it. The roller follows
       on its own: its overlay paints nothing, so whatever --_fill puts on the
       host is what shows through the value area.

       Attribute repeated for the same reason the padding rules above repeat it:
       --border-color is also claimed by the base's invalid rule at identical
       specificity, so without it a disabled *and* aria-invalid stepper draws a
       danger-red capsule whenever another appearance is imported after this
       one. */
    :scope:has(input:disabled)[appearance='pill'] {
      --_fill: var(--l-color-bg-fill-neutral-soft);
      --border-color: var(--l-form-control-disabled-border);
    }
  }
}
