/* nilam — widgets.
 *
 * The three components that need src/behaviours to exist at all. Everything in
 * nilam.components.css works with the stylesheet alone; a combobox, a slider and a
 * two-thumb range do not, because there is no native element to style — <input
 * type="range"> has exactly one thumb, which is why the range is here and not there.
 * That is the whole reason they live in a separate file: importing this one without the
 * module gets you something that looks right and does nothing, and the split makes that
 * obvious rather than surprising.
 *
 * Same layer as the components, so the cascade is unchanged and anything you write
 * unlayered still beats all of it. No new colours: every value below is an existing
 * token, so a re-solve at a different hue moves these too.
 */

@layer nilam.components {
  /* A SENTINEL, so a missing import stops being silent.
   *
   * nilam splits its component layer over two files: components.css for single elements,
   * widgets.css for the three that need src/behaviours. Importing only the first is an easy
   * mistake and, until this line, a completely quiet one — .n-combobox, .n-listbox,
   * .n-slider and .n-range are all plain class names, so nothing throws, nothing warns, and
   * the markup renders as unstyled native controls that still WORK. Someone shipped a
   * range slider that had been a bare browser input the whole time and only found out by
   * screenshot.
   *
   * enhance() reads this property and warns when it wires a widget and the value is absent.
   * A custom property is the only channel available: CSS cannot signal to JS any other way,
   * and this one costs one declaration and nothing at runtime. */
  :root { --n-widgets-loaded: 1; }

  /* ── combobox ─────────────────────────────────────────────────────────────
   *
   * The popup is a plain absolutely-positioned element, NOT a [popover], and that is a
   * decision with a cost worth naming.
   *
   * A popover would get the top layer for free and could never be clipped. But
   * popover="auto" light-dismisses on the first outside pointer-down, which for a
   * combobox includes clicking back into its own input; and the combobox's whole focus
   * design is that DOM focus never leaves the input, which fights the popover focus
   * rules. popover="manual" avoids both and then needs showPopover/hidePopover calls in
   * the module, trading one mechanism for two.
   *
   * So: absolute positioning and a z-index. The cost is real — an ancestor with
   * overflow:hidden will clip the list, and the fix is to not put a combobox inside one.
   */

  .n-combobox {
    position: relative;
    /* isolation, so --z-overlay on the list is measured against this element and not
       against whatever the surrounding page happens to be stacking. Without it a
       neighbouring card with its own z-index can paint over an open list. */
    isolation: isolate;
  }

  /* The select-only shape: a <div role="combobox">, so it needs everything an <input>
     gets from .n-input plus the parts a div has no default for. */
  .n-combobox-value {
    display: flex;
    align-items: center;
    cursor: pointer;
    user-select: none;
    /* The same chevron as .n-select, drawn in currentColor so it follows the mode. A
       hard-coded SVG fill here is invisible in dark mode. */
    padding-inline-end: 2rem;
    background-image:
      linear-gradient(45deg, transparent 50%, currentColor 50%),
      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position:
      calc(100% - 1.125rem) calc(50% + 0.1em),
      calc(100% - 0.75rem) calc(50% + 0.1em);
    background-size: 0.375rem 0.375rem;
    background-repeat: no-repeat;
  }

  /* aria-expanded is the state, so the selector reads it — the same forcing function
     .n-tab uses. A combobox whose expanded state is not in the accessibility tree does
     not get the open border. */
  .n-combobox :is(.n-input, .n-combobox-value)[aria-expanded='true'] {
    border-color: var(--brand-9);
  }

  .n-listbox {
    position: absolute;
    inset-block-start: calc(100% + var(--space-1));
    inset-inline: 0;
    z-index: var(--z-overlay);
    padding: var(--space-1);
    border: var(--line-1) solid var(--neutral-6);
    border-radius: var(--r-4);
    background: var(--surface);
    color: var(--neutral-12);
    box-shadow: var(--shadow-2), var(--rim);
    /* Bounded, and scrollable past the bound. An unbounded list of 300 options runs off
       the bottom of the viewport, and roving.mjs already calls scrollIntoView on the
       active option specifically so this container can scroll. */
    max-block-size: 15rem;
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .n-option {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--r-2);
    font-size: var(--text-0);
    cursor: pointer;
    /* The list is a listbox, not prose. Wrapping an option mid-word turns a keyboard
       cursor into a moving target. */
    white-space: nowrap;
  }

  /* Two states that are NOT the same thing, and conflating them is the commonest
     combobox bug there is:
     *
     *   [data-current]            where the arrow keys are — the visual twin of
     *                             aria-activedescendant. Moves on every keypress.
     *   [aria-selected='true']    the value actually committed. Moves on Enter.
     *
     * They need different weights, because during arrowing both are on screen at once
     * and a user has to be able to tell "I am looking at this" from "I chose this". */
  /* The tint alone is not a state in dark mode. --neutral-3 on --surface is 1.0009:1
     there, because dark --surface is DEFINED as step 3's lightness with the tint
     removed — so the fill differs by chroma and by nothing else. The inset ring is the
     second channel: --neutral-6 measures 1.3235:1 on a card in dark and 1.6435:1 in
     light, which is above the 1.1072:1 ceiling any fill can reach. See the note on
     .n-slider-track for the sweep. */
  .n-option:hover {
    background: var(--neutral-3);
    box-shadow: inset 0 0 0 var(--line-1) var(--neutral-6);
  }
  .n-option[data-current] {
    background: var(--neutral-4);
    /* An inset bar rather than only a background, because the difference between
       neutral-3 and neutral-4 is one contrast step by construction — deliberately small,
       since the ramp is solved — and one step is not enough to carry "this is the
       cursor" on its own. Same reasoning as the status glyphs: two channels, not one. */
    box-shadow: inset 0.125rem 0 0 0 var(--brand-9);
  }
  .n-option[aria-selected='true'] {
    color: var(--brand-11);
    font-weight: var(--weight-medium);
  }
  /* And the non-hue channel for selection, for the same reason every status component in
     this system carries a glyph: colour alone is WCAG 1.4.1. */
  .n-option[aria-selected='true']::after {
    content: '✓';
    margin-inline-start: auto;
    padding-inline-start: var(--space-3);
    font-size: 0.8em;
  }
  .n-option[aria-disabled='true'] {
    color: var(--neutral-11);
    cursor: not-allowed;
  }

  /* Result count for the filter. .n-sr-only in markup if you only want it announced;
     this styles the visible case. */
  .n-combobox-status { font-size: var(--text-00); color: var(--neutral-11); }

  /* ── slider ───────────────────────────────────────────────────────────────
   *
   * --n-slider-pct is written by slider.mjs and read by both the fill and the thumb, so
   * all the geometry stays here. Nothing in the module sets `inset-inline-start` or a
   * width directly — which is what lets RTL and the vertical variant be a CSS concern.
   */

  .n-slider {
    position: relative;
    display: flex;
    align-items: center;
    inline-size: 100%;
    block-size: 1.25rem;
    /* Without this a drag on a touch screen scrolls the page instead of moving the thumb,
       and the slider is simply not operable by finger. */
    touch-action: none;
  }

  /* THE BORDER IS THE EXTENT, and this component was the one that did not have it.
   *
   * .n-progress, .n-bar and .n-meter all carry a --neutral-7 boundary — added when a
   * real app shipped a meter whose track measured 1.08:1 against its card. The slider
   * track was missed in that pass, so it kept a bare fill: --neutral-4 on --surface
   * measures 1.2722:1 in light and 1.0844:1 in DARK. The filled part is --brand-9 and
   * perfectly visible, so what disappears is the remainder — a user can see how far the
   * value has come and not how far it can go. The track is the component's boundary, so
   * that is WCAG 1.4.11 at 3:1: --neutral-7 measures 3.6537:1 on a card in light and
   * 3.0469:1 in dark.
   *
   * A better FILL cannot fix this, which is worth stating because it is the obvious
   * thing to try. Sweeping all 2001 lightnesses, the best any fill can do against both
   * the page and a card in dark while keeping body text at 4.5:1 is 1.1072:1 — and the
   * optimum is pure black. Near the page WCAG's +0.05 flare term swamps the lightness
   * difference, so the room simply is not there. --void is used here because it is the
   * one surface defined as below the page, not because it rescues the ratio. */
  .n-slider-track {
    inline-size: 100%;
    block-size: 0.375rem;
    border: var(--line-1) solid var(--neutral-7);
    border-radius: var(--r-full);
    background: var(--void);
    overflow: hidden;
  }

  .n-slider-fill {
    block-size: 100%;
    inline-size: var(--n-slider-pct, 0%);
    border-radius: inherit;
    background: var(--brand-9);
  }

  .n-slider-thumb {
    position: absolute;
    inset-inline-start: var(--n-slider-pct, 0%);
    translate: -50% 0;
    inline-size: 1rem;
    block-size: 1rem;
    border: var(--line-2) solid var(--brand-9);
    border-radius: var(--r-full);
    /* --surface, not --neutral-1: on a card the thumb has to read as the card's own
       colour or it looks like a hole. */
    background: var(--surface);
    box-shadow: var(--shadow-1);
    cursor: grab;
  }
  .n-slider-thumb:active { cursor: grabbing; }

  /* 44px pointer target on a 16px thumb — WCAG 2.5.8. Same ::after trick as .n-btn, so
     the painted thumb stays the size the design wants and layout is untouched. */
  .n-slider-thumb::after {
    content: '';
    position: absolute;
    inset: 50% 50% auto auto;
    translate: 50% -50%;
    inline-size: 2.75rem;
    block-size: 2.75rem;
  }

  /* --brand-9, not --brand-6, and with a gap.
   *
   * This replaced the global ring with a --brand-6 halo, which measures 1.6435:1 against a
   * card in light and 1.3235:1 in DARK. Nothing else about the thumb changes on focus — the
   * border is --brand-9 focused and unfocused — so at 1.32:1 a keyboard user in dark mode
   * had no visible focus state at all. WCAG 2.4.7.
   *
   * Drawn as a shadow rather than an outline for the reason .n-input gives (it hugs the
   * radius and survives a scroll container), but in two rings: a --surface gap first, then
   * --brand-9 at 4.9408:1. The gap is what stops a --brand-9 ring merging into the thumb's
   * own --brand-9 border and reading as one slightly fatter thumb. */
  .n-slider-thumb:focus-visible {
    outline: none;
    box-shadow:
      0 0 0 var(--ring-offset) var(--surface),
      0 0 0 calc(var(--ring-offset) + var(--ring-width)) var(--brand-9),
      var(--shadow-1);
  }

  .n-slider-thumb[aria-disabled='true'] {
    /* Not opacity — the same rule the buttons follow. Fading a control fades whatever is
       drawn on it too, and this one has a border doing the work. */
    border-color: var(--neutral-7);
    background: var(--neutral-3);
    cursor: not-allowed;
  }
  .n-slider:has([aria-disabled='true']) .n-slider-fill { background: var(--neutral-7); }

  /* Vertical, via :has() reading the thumb's own aria-orientation. The attribute is what
     slider.mjs reads to decide which pointer axis to measure, so making the CSS read the
     same attribute means the two cannot disagree — a .n-slider-vert class could. */
  .n-slider:has([aria-orientation='vertical']) {
    inline-size: 1.25rem;
    block-size: 9rem;
    flex-direction: column;
    justify-content: flex-end;
  }
  .n-slider:has([aria-orientation='vertical']) .n-slider-track {
    inline-size: 0.375rem;
    block-size: 100%;
    display: flex;
    align-items: flex-end;
  }
  .n-slider:has([aria-orientation='vertical']) .n-slider-fill {
    inline-size: 100%;
    block-size: var(--n-slider-pct, 0%);
    align-self: flex-end;
  }
  .n-slider:has([aria-orientation='vertical']) .n-slider-thumb {
    inset-inline-start: 50%;
    inset-block-end: var(--n-slider-pct, 0%);
    translate: -50% 50%;
  }

  /* The visible value beside a slider. Tabular figures so the row does not jitter by a
     pixel every time a digit changes width while dragging. */
  .n-slider-value {
    font-size: var(--text-0);
    font-variant-numeric: tabular-nums;
    color: var(--neutral-12);
    min-inline-size: 3.5ch;
    text-align: end;
  }

  /* ── range, two thumbs ────────────────────────────────────────────────────
   *
   * --n-range-from and --n-range-to are written by range.mjs, both on the wrapper, and read
   * by the selected span and by both thumbs — same arrangement as --n-slider-pct, and for
   * the same reason: no `inset-inline-start` and no width is set from JavaScript, so RTL and
   * the vertical variant stay CSS problems.
   *
   * Which property a thumb reads is chosen by [data-n-range], which range.mjs stamps from
   * DOM order. Same forcing function as aria-orientation below: the stylesheet reads what
   * the module wrote, so the two cannot end up disagreeing about which thumb is the lower.
   */

  .n-range {
    position: relative;
    display: flex;
    align-items: center;
    inline-size: 100%;
    block-size: 1.25rem;
    /* Without this a drag on a touch screen scrolls the page instead of moving a thumb, and
       the control is not operable by finger at all. */
    touch-action: none;
  }

  /* The border is the extent, exactly as on .n-slider-track above, and here it is load-
     bearing twice over: a range's fill is in the MIDDLE of the track, so both remainders
     disappear without it and a user can see the span they picked but neither end it could
     reach. Measured on a card: --neutral-4 is 1.0844:1 in dark and --neutral-3 is 1.0009:1,
     against the 3:1 WCAG 1.4.11 asks of a control's boundary. --neutral-7 is 3.0469:1 in
     dark and 3.6537:1 in light. --void for the fill, being the one surface defined as below
     the page — the border is what carries the ratio. */
  .n-range-track {
    position: relative;
    inline-size: 100%;
    block-size: 0.375rem;
    border: var(--line-1) solid var(--neutral-7);
    border-radius: var(--r-full);
    background: var(--void);
    overflow: hidden;
  }

  /* The selected span. Absolutely positioned rather than a flow child with a percentage
     margin, because percentage margins resolve against the containing block's INLINE size in
     both axes — which would silently make the vertical variant a few pixels tall. */
  .n-range-sel {
    position: absolute;
    inset-block: 0;
    inset-inline-start: var(--n-range-from, 0%);
    inline-size: calc(var(--n-range-to, 100%) - var(--n-range-from, 0%));
    background: var(--brand-9);
  }

  .n-range-thumb {
    position: absolute;
    inset-inline-start: var(--n-range-from, 0%);
    translate: -50% 0;
    inline-size: 1rem;
    block-size: 1rem;
    border: var(--line-2) solid var(--brand-9);
    border-radius: var(--r-full);
    /* --surface, not --neutral-1: on a card the thumb has to read as the card's own colour
       or it looks like a hole. */
    background: var(--surface);
    box-shadow: var(--shadow-1);
    cursor: grab;
  }
  .n-range-thumb[data-n-range='to'] { inset-inline-start: var(--n-range-to, 100%); }
  .n-range-thumb:active { cursor: grabbing; }

  /* 44px pointer target on a 16px thumb — WCAG 2.5.8, same ::after trick as .n-btn and the
     slider thumb. Two of them overlap when the range closes to nothing; that is fine,
     because pointerdown picks the nearer thumb by value rather than by hit target. */
  .n-range-thumb::after {
    content: '';
    position: absolute;
    inset: 50% 50% auto auto;
    translate: 50% -50%;
    inline-size: 2.75rem;
    block-size: 2.75rem;
  }

  /* The slider thumb's ring, unchanged: --brand-9 at 4.9408:1 with a --surface gap first, so
     the ring cannot merge into the thumb's own --brand-9 border and read as one fatter
     thumb. A --brand-6 halo measures 1.3235:1 on a card in dark, which is no focus state at
     all — WCAG 2.4.7. */
  .n-range-thumb:focus-visible {
    outline: none;
    box-shadow:
      0 0 0 var(--ring-offset) var(--surface),
      0 0 0 calc(var(--ring-offset) + var(--ring-width)) var(--brand-9),
      var(--shadow-1);
  }

  .n-range-thumb[aria-disabled='true'] {
    /* Not opacity — fading a control fades whatever is drawn on it, and the border is what
       is doing the work here. */
    border-color: var(--neutral-7);
    background: var(--neutral-3);
    cursor: not-allowed;
  }
  .n-range:has([aria-disabled='true']) .n-range-sel { background: var(--neutral-7); }

  /* Vertical, via :has() reading a thumb's own aria-orientation — the same attribute
     range.mjs reads to choose which pointer axis to measure, so the two cannot disagree. */
  .n-range:has([aria-orientation='vertical']) {
    inline-size: 1.25rem;
    block-size: 9rem;
    flex-direction: column;
  }
  .n-range:has([aria-orientation='vertical']) .n-range-track {
    inline-size: 0.375rem;
    block-size: 100%;
  }
  .n-range:has([aria-orientation='vertical']) .n-range-sel {
    inset-block: auto var(--n-range-from, 0%);
    inset-inline: 0;
    inline-size: auto;
    block-size: calc(var(--n-range-to, 100%) - var(--n-range-from, 0%));
  }
  .n-range:has([aria-orientation='vertical']) .n-range-thumb {
    inset-inline-start: 50%;
    inset-block-end: var(--n-range-from, 0%);
    translate: -50% 50%;
  }
  .n-range:has([aria-orientation='vertical']) .n-range-thumb[data-n-range='to'] {
    inset-block-end: var(--n-range-to, 100%);
  }
}
